В этой статье мы разработаем стандартный модуль с целью описать алгоритм и конкретные приемы создания модулей. Стандартным мы называем модуль, потому что он будет иметь свой раздел в административной и клиентской части.
Назовем модуль “Список сети магазинов” (shoplist). Модуль должен предоставлять возможность администратору добавлять, удалять, изменять(в том числе массово изменять) магазины по заданной форме, через административную панель. Пользователям должен отображаться список магазинов в отдельном разделе на сайте. Администратор должен иметь возможность импортировать/экспортировать список магазинов в формате CSV. Также администратор должен иметь возможность фильтровать список магазинов сети по городу в админ. панели.
Поля данных магазина: Город, Адрес, Телефоны, Время работы, Описание, Фото фасада.
Сделаем модуль мультисайтовым, чтобы на каждом сайте можно было разместить независимый список сети магазинов.
Как видит данную задачу разработчик?
Создать:
- Пустышку модуля(структуру папок с минимальным набором файлов).
- Класс ORM объекта «Магазин»
- Класс API для работы с ORM объектами «Магазин»
- Создать контроллер для административной панели с CRUD действиями, чтобы администратор мог добавлять магазины.
- Создать схему импорта/экспорта CSV, чтобы активировать возможность обмена данными в формате CSV
- Создать фронт контроллер и шаблон, чтобы пользователи могли видеть список.
- Создать маршрут, чтобы фронт контроллер откликался на URL
Создаем базовую структуру модуля
В папке /modules создаем следующую структуру папок:
-
shoplist //Корневая папка модуля
-
config //Папка для конфигурационных классов
-
controller //Папка для классов контроллеров
-
admin //Папка для классов контроллеров админ. панели
-
front //Папка для классов фронт-контроллеров клиентской части
-
-
model //Папка для моделей данных
-
csvschema //Папка для схемы обмена в формате csv
-
orm //Папка для классов ORM объектов
-
-
view //Папка для шаблонов отображения
-
Чтобы модуль корректно был идентифицирован системой, в папке shoplist/config необходимо создать файл file.inc.php, содержащий базовые сведения о модуле и его настройки.
Файл конфигурации - это обычный объект ORM в ReadyScript, у которого установлен особый класс хранилища и который имеет свой набор обязательных полей(name, description, version, author). Эти параметры установлены в классе \RS\Orm\ConfigObject, поэтому нам нужно использовать его в качестве родительского, для создания собственного класса конфигурации.
↑ Файл shoplist/config/file.inc.php
Начиная с версии 2.0.0.136 от 13.04.2015 сведения о модуле и его параметрах по-умолчанию хранятся в отдельном файле module.xml
Файл должен содержать 4 обязательных секции - name, description, version, author. Остальные секции - необязательные и используются для установки значений по-умолчанию для параметров, описанных в конфигурационном классе ShopList\Config\File
↑ Файл shoplist/config/module.xml
Чтобы добавить пункт меню "Сеть магазинов" в административную панель, модуль должен обрабатывать событие getmenus. Для удобства, в ReadyScript все обработчики событий описываются в одном файле handlers.inc.php, расположенном в папке ИМЯ_МОДУЛЯ/config.
↑ Файл shoplist/config/handlers.inc.php
Создаем ORM объект «магазин»
Так как задача подразумевает хранение данных в базе, нам потребуется создать класс ORM объектов. В ReadyScript не принято работать с базой без ORM объектов, как минимум по трем причинам:
- ORM объекты создают и поддерживают в актуальном состоянии базу данных при установке и обновлении модуля, (не нужно нигде писать SQL запросы для создания таблиц)
- ORM объекты автоматически генерируют формы для сущностей в админ. панели.
- Удобство пользования ORM объектом (возможность добавления произвольных методов, чего не сделаешь с массивом) при работе как в PHP коде, так и в шаблонах
Все сведения о магазинах мы будем хранить в отдельной таблице. Опишем ORM объект для нашей сущности "Магазин":
↑ Файл shoplist/model/orm/shopitem.inc.php
Наверное, стоит сказать несколько слов об использовании ORM объектов в ReadyScript. Каждый ORM Объект имплементирует интерфейс ArrayAccess, это означает, что к его свойствам можно обращаться как к элементам массива. Например:
↑ Примеры работы с ORM объектами
Создаем класс с API функциями для нашего модуля
ReadyScript предполагает, что у каждого модуля должно быть высокоуровневое API, позволяющее выбирать и записывать данные, абстрагируясь от типа хранилища.
Именно с этими API будут работать стандартные CRUD контроллеры административной панели. ReadyScript предоставляет базовые классы API для различных ситуаций.
Класс \RS\Module\AbstractModel\EntityList - предоставляет возможность простой работы с плоскими списками. То есть данный класс содержит реализацию следующих действий: создание, удаление, редактирование, групповое редактирование, установка связи с визульными фильтрами плоских(не древовидных) объектов. Достаточно просто сообщить с каким ORM объектом следует работать данному API.
Создадим класс с API функциями работы с нашим объектом. Файл должен находиться в папке shoplist\model. Назовем класс Api, а файл соответственно api.inc.php.
↑ Файл shoplist/model/api.inc.php
ReadyScript считает, что контроллеры должны быть минималистичными, а классы моделей могут быть весьма объемными. Поэтому если в будущем, нам понадобиться реализовать какой-то функционал, мы не задумываясь разместим его в классе Api нашего модуля.
Создаем контроллер для административной панели
Наступает самая интересная часть разработки. Создание контроллера, который позволит нам запустить полноценную работу модуля в административной панели. Сразу отмечу, что мы создадим стандартный раздел в админ. панели, который ничем не будет отличаться от тех разделов, которые мы создаем для наших системных модулей. В разделе будет полностью работать все ajax функции.
Итак, создадим файл control.inc.php в папке shoplist/controller/admin.
↑ Файл shoplist/controller/admin/control.inc.php
Рассмотрим подробнее код контроллера административной панели. Большинство задач административной панели сводится к выполнению стандартных CRUD(Create, Read, Update, Delete) операций. Чтобы не описывать все действия с нуля, унаследуем наш контроллер от класса \RS\Controller\Admin\Crud. Благо он уже содержит все что нам нужно.
Стоит сказать, что класс \RS\Controller\Admin\Crud имеет немного нестандартный ход выполнения action'а контроллера. Если обычно, для выполнения действия(action'а) ищется метод с префиксом action. Например если для выполнения действия Index обычный контроллер ReadyScript будет искать у себя метод с именем actionIndex, то Crud сперва будет искать метод helperIndex, выполнять его, а лишь затем контроллер будет искать метод actionIndex для выполнения.
Для чего это сделано? Дело в том, что метод helperIndex запускается чтобы заполнить данными helper визуальной части. Такой helper уже знает о всех кнопках в панелях инструментов, о структуре таблиц, фильтров, и.т.д. Сразу после запуска метода helperIndex, CRUD контроллер бросает в систему событие с именем controller.exec.КОРОТКОЕ_ИМЯ_КОНТРОЛЛЕРА.ИМЯ_ДЕЙСТВИЯ, чтобы другие модули могли внести изменения в этот helper, например добавить свою кнопку или свою колонку в таблицу и.т.д. Затем запускается уже actionIndex, который использует раннее сформированный хелпер для построения HTML выдачи.
В нашем случае мы используем реализацию всех action'ов от класса CRUD. В конструкторе контроллера мы задали API, через которые происходит взаимодействие с моделью данных. Также мы описали структуру колонок таблицы и фильтра, которые будут отображены на странице.
Модуль уже можно установить. Чтобы установить модуль необходимо зайти в административную панель в раздел Веб-сайт → Настройка модулей. В списке модулей, в строке неустановленного модуля будет пометка "не установлен", кликнув на которую произойдет его установка. Сразу после установки, обновите страницу вашего браузера, нажав F5. Наш модуль появится в меню Разное. В админ. панели будут доступны следующие страницы нашего модуля:
Табличное представление
Редактирование карточки магазина
Групповое редактирование карточек магазинов
Схема импорта/экспорта в CSV
ReadyScript позволяет за считанные минуты создать схему импорта и экспорта в формате CSV, используя уникальную технологию пресетов. Каждый пресет добавляет различное количество колонок в схему импорта экспорта данных. Так как в контроллере мы объявили, что будем использовать схему CSV с названием ShopItem, нам следует создать файл shopitem.inc.php в папке shoplist/model/csvschema, следующего содержания:
↑ Файл shoplist/model/csvschema/shopitem.inc.php
Так как наш ORM-объект является достаточно простым, то в схеме обмена нам будет достаточно использовать два пресета:
- Preset\Base - добавляет в схему экспорта колонки, соответствующие полям ORM Объекта
- Preset\SinglePhoto - добавляет в схему экспорта колонку Фото, которую данный пресет потом также будет корректно обрабатывать при импорте.
CSV не должен содержать никаких ID, поэтому исключим поля id, site_id из экспорта. В момент импорта id каждой записи будет сгенерирован, а в поле site_id будет записан id сайта, выбранного в момент импорта данных из CSV. Так как поле image будет обрабатываться отдельным пресетом SinglePhoto, исключим его из пресета Base также. И на последок мы задаем по каким полям сперва пытаться загрузить объект. Если объект будет загружен, то произойдет его обновление. Это позволит избежать дублирования строк в базе при повторном импорте CSV. В итоге мы можем наблюдать следующий диалог, если нажмем на кнопку экспорт данных.
Диалог экспорта данных
Создание клиентского фронт контроллера
После того как административная часть вся реализована, можно перейти к созданию клиентского контроллера. Стоит заметить, что в ReadyScript есть 2 понятия контроллеров: фронт-контроллеры и блок-контроллеры.
Фронт контроллеры, это те, что получают управление сразу при открытии определенного URL. Фронт контроллеры отрабатывают и формируют так называемое "главное содержимое страницы", которое затем обычно отображается в центральной зоне страницы. Фронт контроллер может решить, продолжать ли рендеринг страницы или вернуть, например ошибку 404, или вернуть поток данных, не оборачивая их HTML-кодом.
Блок контроллеры, запускаются в процессе рендеринга определенного шаблона или страницы, в том порядке в котором они расположены в шаблоне или в Конструкторе блоков. Блок контроллеры отрабатывая, формируют какой-то участок HTML кода страницы. HTML код блок контроллеров всегда будет являться лишь частью страницы, т.к. до них другие модули, возможно уже сформировали вывод и они не могут отменить его.
В нашем модуле мы создадим только фронт контроллер, выводящий список магазинов. Так как для нашей страницы "список магазинов сети", мы не будем задавать индивидуальное оформление в Конструкторе сайта, то для нашего контроллера будет использоваться оформление "страницы по-умолчанию". Как видно из разметки "страницы по-умолчанию" в Конструкторе сайта, "главное содержимое страницы", которое сформирует наш фронт-контроллер будет отображаться в центральной части страницы. Создаем файл shops.inc.php в папке shoplist/controller/front/shops.inc.php со следующим содержимым.
↑ Файл shoplist/controller/front/shops.inc.php
Как видно, наш контроллер подключает шаблон shops.tpl. Это означает, что система сперва будет искать шаблон в папке темы /templates/ТЕКУЩАЯ_ТЕМА/moduleview/shoplist/shops.tpl, а в случае неудачи поиск будет произведен в папке модуля /modules/shoplist/view/shops.tpl Модуль должен содержать в своей папке view эталонный шаблон отображения, чтобы в дальнейшем разработчики темы оформления могли опираться на этот шаблон при построении собственного. Разместим файл shops.tpl в папке shoplist/view/.
↑ Файл shoplist/view/shops.tpl
В шаблоне подключается CSS файл с помощью конструкции {addcss file="{$mod_css}shoplist.css" basepath="root"} в переменной mod_css находится путь к папке shoplist/view/css/, в которой должны находиться css файлы модуля.
basepath, задает каталог, который будет приписан слева к значению атрибута file. Если атрибут не указан, то используется значение theme.
- root - означает, что к file не будет приписано ничего
- theme - добавляет путь к CSS каталогу текущей темы.
- common - добавляет путь к общему CSS каталогу (/resource/css/)
Создаем CSS файл, для более приятного отображения со стандартной темой.
↑ Файл shoplist/view/css/shoplist.css
Создание маршрута для фронт-контроллера
Для понимания смысла маршрутов стоит рассмотреть полный цикл формирования страницы от самого запроса на сервер до отдачи контента.
- Сервер получает запрос, который обрабатывается единой точкой входа - файлом /index.php
- Управление получает диспетчер маршрутов
- В системе происходит событие getroute, на которое каждый модуль возвращает свои маршруты.
- Диспетчер перебирает маршруты, сопоставляя URI с маской, указанной в маршруте.
- Как только подходящий маршрут найден, он возвращает название фронт-контроллера и действия, которому нужно передать управление
- Выполняется действие фронт-контроллера, результат возвращается в браузер
Маршрут - описывает связь определенных URI и фронт-контроллеров. Чтобы сообщить системе о своем маршруте нужно установить обработчик события getroute. Опишем наш обработчик событий:
↑ Файл shoplist/config/handlers.inc.php
Как видно, наш маршрут сообщил системе, что при открытии пользователем адреса /shops/ или /shops/параметр/ управление должно переходить к контроллеру \ShopList\Controller\Front\Shops, учитывая то, что не задан какой-то определенный action, система будет запускать action по-умолчанию - Index, поэтому мы реализовали в контроллере выше один метод actionIndex.
После создания маршрута, если набрать в браузере: ваш_домен/shops/ , то можно будет увидеть список магазинов сети, заданных в административной панели.
Используя маску, которую разработчик описывает единожды, система позволяет строить все ссылки внутри сайта. Например:
Для вызова этой конструкции из шаблона, воспользуйтесь переменной $router (равна \RS\Router\Manager::obj()), которая присутствует всегда в любом шаблоне.
Заключение
Полученную папку с модулем можно поместить в zip архив и легко устанавливать на любой платформе ReadyScript, через раздел Веб-сайт -> Настройка модулей -> Добавить модуль.
Мы постарались предусмотреть множество кейсов использования системы, и создали набор базовых классов, с помощью которых можно быстро реализовать различный классический функционал. В случае, если нужно сделать что-нибудь специфичное, базовый функционал можно расширять с помощью механизмов ООП.
Как видно, с помощью небольшого количества кода, можно создавать максимально функциональные разделы в административной панели. Разработчику при этом не нужно погружаться в HTML, JavaScript, а нужно работать только с объектами PHP.
Модуль, созданный в процессе написания данной статьи можно скачать здесь.