Администрирование модулей в InlifeCMS

Общая информация о модулях

Да, начинать надо, как это не грустно, с описания.. Нужно написать для чего модуль предназначен, что может, как делать вещи правильно и какими средствами. Не нужно расписывать сразу все подробно, надо набросать скелет (предназначение и возможности нужно описать сразу), а после реализации или в процессе можно дописывать подробности. Желательно также сразу записать неочевидные возможности, т.к. потом это может забыться. Могу сказать, что при реализации поддержки модулей был избран именно этот подход. Он вполне себя оправдал, так как позволил обдумать большую часть деталей заранее, а также держать перед глазами весь план реализации.

Структура

Модуль включает в себя следующие объекты:

  • container - контейнер для привязки к каталогу сайта (ресурс в DSMv2)
  • controller - контроллер модуля (менеджер+вьюер)
  • viewer - вьюер модуля (будет слит с controller)
  • api - API модуля (extension в DSMv2)
  • config - конфиг модуля
  • db_tables - описание таблиц базы данных и их минимальное наполнение (возможно, что-то еще - view, triggers, SP, etc..)
  • db_updates - файл с историей изменения таблиц модуля
  • modinfo - файл с описанием модуля
  • templates - каталог с шаблонами

В поле ModuleManager::$required_mod_objects перечислены обязательные объекты модуля:

<?
var $required_mod_objects = array(
            'container', 'controller', 'api', 'viewer', 'config', 'db_tables'
, 'db_update', 'modinfo' ); ?>

Помимо обязательных, поддерживаются опциональные subapi: hook_created и hook_updated. Их задача - выполнение необходимых действий после добавления и обновления модуля соответственно.

Каждый объект - это псевдоним для определенного файла, псевдонимы определены в ModuleManager::$mod_objects:

<?
var $mod_objects = array(
            'container'     => 'container.php',
            'controller'    => 'controller.php',
            'api'           => 'api.php',
            'viewer'        => 'viewer.php',
            'config'        => 'module.ini',
            'db_tables'     => 'setup/tables.sql',
            'db_updates'    => 'setup/db-update.sql',
            'modinfo'       => 'setup/module.info',
            'plugins'       => 'plugins'
        );
?>  

Т.о. структура каталога модуля может выглядеть так:

В каталоге setup находятся файлы, необходимые для инсталяции и, возможно, некоторые дефолтные данные, использующиеся в работе.

public: доступ к статическим файлам модуля

Каталог public предназначен для хранения файлов, которые должны быть доступны через Web: css, js и т.д. Доступ к файлам можно получить по адресу /mod/files/<module_name>/<file_name>.
При наличии в каталоге public стилевого файла и(или) библиотеки скриптов (их имена фиксированы - module.css и module.js соответственно)
их подключение происходит автоматически без дополнительных усилий со стороны разработчика

Конфиг модуля

При загрузке конфига модуля его данные сливаются с дефолтным конфигом из каталога setup ядра.
Персональные настройки сайта должны быть заданы в etc/engine.ini

modinfo: описание модуля


Атрибут requires задает список зависимостей, которые нужны для работы модулю. Могут быть заданы версии модулей или ядра. Также поддерживается специальный тип зависимости «тегированная зависимость», на данный момент это только проверка наличия в ОС заданного исполняемого файла.

При использовании свежих фич из ядра, нужно прописать в requires модуля соответствующую версию ядра:

посмотреть версию ядра, на которой вы отлаживаете модуль, можно так:

Пример:

Требуются:

  • ядро (сore) не младше билда 20080724;
  • модуль estimation_controller версии 1.3;
  • модули object_access, qualifier любой версии
  • исполняемые файлы ffmpeg, composite2, convert (файлы ищутся по путям, заданным переменной окружения PATH)

Атрибут build задает дату модификации модуля. При обновлении модуля делается проверка на то, что этот build нового модуля превышает build старого.
При коммитах кода модулей необходимо актуализировать эту информацию. Предлагается следующий формат: <build>YYYYMMDDnn</build>, где YYYYMMDD - дата, nn - инкрементальный счетчик обновлений на заданную дату. Т.е. если коммит для этого модуля сегодня не выполнялся, то nn=00, при последующих коммитах nn должно увеличиваться на 1. На следующий день nn должно обнулиться.

Проверка на корректность структуры

Для проверки наличия всех необходимых частей модуля можно использовать опцию -t утилиты module.php. В качестве параметра опции должен быть передан префикс каталога модуля (т.е. имя каталога без версии), будет найдена последняя версия, для которой и выполнится проверка. На данных момент проверяется наличие объектов из массива ModuleManager::required_mod_objects, существование ожидаемых классов объектов из ModuleManager::checked_mod_objects, а также всех SubAPI.

Управление

Управление модулями, подключенными к ядру сайта, осуществляется через утилиту module.php, которая лежит в каталоге cur/bin.

Обновление модуля

  1. распаковка нового модуля
  2. проверка модуля (какие проверки можно заюзать?)
  3. начало транзакции и блокировка модуля (поле blocked)
    Теперь при обращении к модулю будет выдаваться сообщение о недоступности для всех пользователей, кроме администраторов (группа admins (gid=1))
  4. обновление базы до версии, требуемой новым модулем
  5. обновление информации о модуле (версия модуля, версия структуры базы и т.д.)
  6. вызов subapi hook_updated, если таковой предоставляется модулем, для выполнения внутренних действий самим модулем.
  7. коммит транзакции и разблокировка модуля

API ядра

В файле lib/module.class.php находится класс ModuleManager, осуществляющий программное управление модулями и их частями. Все операции с модулями выполняются с помощью него.

  • object ModuleManager::CreateModuleObject($modname, $aobject, $params=false);

    Метод используется для создания объекта модуля: container|controller|viewer|api (viewer - поддерживается временно, в дальнейшем будет слит с controller)
    Если запрошенный модуль не проинсталирован, будет сгенерировано исключение путем вызова mod_error. Если код предполагает возможность отсутствия модуля, возникновения исключительной ситуации можно избежать путем проверки наличия модуля:

    <?
    if($modmanager->FindModuleIntoDB('имя модуля')) {
          $api =& $modmanager->CreateModuleObject('имя модуля', 'api');
    }
    ?>  

  • array ModuleManager::GetAvailableModules($scope=null); - получить список доступных модулей. scope := internal | external;

Реализация

Имена классов должны соответствовать соглашению: <modname>_<object>. Классы нужно наследовать соответственно от Container, Viewer и Controller.
В Container реализован механизм создания объектов модуля (см. реализацию Container::_GetObject()).
В контроллере (и вьюере) должна содержаться лишь логика интерфейса пользователя! Вся бизнес-логика (то, что относится непосредственно к обработке данных модуля) должна быть помещена в API-объект! Иначе возникнут проблемы с повторным использованием кода. (См, например, реализацию goodgroup и pricelist)

Для указания путей к базовым шаблонам модуля поддерживается тег {module}, который заменяется путем к корневому каталогу модуля. Например, путь вида '{module}/templates/account_manager.tpl' указывает на шаблон account_manager.tpl, лежащий в каталоге modhome/templates.

Если необходимо использовать шаблоны в одном из классов API (т.е. класс наследует Widget), то необходимо задать свойство $module для того, чтобы Widget знал, какой модуль используется при разрешении тега {module} в путь.

Конфигурирование контейнера необходимо производить в методе handler_config, а Edit оставить только для редактирования контейнера (см. mod_skel/controller.php). В будущем config станет одним из предопределенных методов наряду с edit, add и т.д.

Если класс API модуля наследуется от Module_API, то ему (при создании через CreateModuleObject) передается ссылка на инстанс ModuleManager, которую можно найти в $this->modmanager.

База данных

Все таблицы, относящиеся к модулю, должны иметь префикс с именем этого модуля. Например для форума:
forum_message
forum_topic
и т.д.
Это нужно для устранения возможных конфликтов имен между одноименными таблицами разных модулей.

Не забываем помещать в начало setup/tables.sql тег ревизии файла $Id:$! Без этого обновление структуры таблиц модуля работать не будет.

Версионность

На данный момент версия берется из названия каталога, в котором лежит модуль, при поиске последней версии. В дальнейшем планируется добавление этой информации в setup/module.info.

На данный момент в module.info введен атрибут build, который представлет собой метку даты и счетчик обновлений от 0 до 99 (см. описание build выше в разделе «Структура»). При любых обновлениях кода атрибут build необходимо увеличивать - это даст гарантию того, что везде будет одно и тоже значение build (по факту это версия модуля)

Версии имеют следующий вид: <major>.<minor>.<fix level>

Пример:
mod_megathing-2.3.12 - модуль megathing, major-версия 2, minor-версия 3, подверсия с исправлениями 12.

Major-номер задает основную версию. Смена этого номера должна происходить при серьезных переработках кода или добавлении большого количества кода. Minor-версия меняется при добавлении функционала среднего и малоого объема, серьезных фиксах. При этом поведение кода не должно сильно меняться (иначе, это кандидат на смену major-номера). Подверсии fix level должны нести в себе только фиксы ошибок.

При выпуске новой major.minor-версий необходимо делать ветку в CVS. При смене fix level необходимо помечать код соответствующим тегом.

Временные файлы

Если необходимо использовать временные файлы, их нужно хранить в каталоге Paths::tmp_dir. В этом случае такие файлы будут удаляться автоматически.
Автоматическая чистка запускается ночью и стирает файлы, которые не модифицировались более 4 часов.

Организационные вопросы

GIT

Каталоги, в которых лежат модули, должны иметь префикс mod_. В репозитории git все модули лежат без номеров версий.

Допустим, спрограммирован новый модуль mod_newmod-1.0, и ему место в репозитории. Новый модуль в репозитории заводить так:

proba# cd /rep && mkdir mod_newmod.git && cd mod_newmod.git && git init --bare

Будет создан пустой реп без working directory - специально для центральных репозиториев

Далее идем туда, где ваш локальный модуль:

# cd ....mod_newmod-1.0 && git init && git add . && git commit -m initial && git remote add origin ssh://proba/rep/mod_newmod.git && git push origin master

Везде newmod это название нового модуля, а не лежащая где-то директория.

Обновление кода в репозитории из текущей папки модуля:

proba# git commit -a && git push origin

Обновление кода модуля из репозитория:

proba# git pull

Дополнительные информационные файлы модуля

Желательно вести историю изменений модуля в файле CHANGELOG в корне каталога модуля. Информация, заносимая в этот файл, должна содержать краткое описание произведенных изменений.
Также может возникнуть необходимость сохранения описания модуля (файл README), руководства по инсталяции и обновлению (INSTALL).