Версия: 5.x
Модульность

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

Модули позволяют расширять функционал ReadyScript и вмешиваться в стандартный ход выполнения различных операций. Каждый модуль включает в себя классы контроллеров, моделей, шаблоны отображения, изображения, CSS стили, JS файлы, языковые файлы. Такая инкапсуляция позволяет легко переносить модуль между приложениями на платформе ReadyScript. Модуль в ReadyScript - это папка с определенной структурой данных внутри. Модуль обязательно должен располагаться в каталоге для модулей /modules и иметь следующую структуру.

  • modulename //название модуля. Можно использовать любое английское имя
    • config //Здесь должны находиться конфигурационные классы модуля
      • file.inc.php //Конфигурационный файл модуля
      • module.xml //Файл со сведениями о модуле и его настройках по-умолчанию
      • handlers.inc.php //Файл с обработчиками событий, на которые подписан модуль
      • install.inc.php //Файл с инструкциями по установке модуля
      • uninstall.inc.php //Файл с инструкциями по удалению модуля
      • patches.inc.php //Файл для единоразовых корректировок системы
    • controller //Контроллеры модуля Main
      • admin //Корневая папка для контроллеров административной части
      • front //Корневая папка для фронт контроллеров
      • block //Корневая папка для блочных контроллеров
    • model //Корневая папка для моделей
      • orm //Корневая папка для ORM объектов
      • commenttype //Папка для классов, описывающих тип комментариев
      • csvschema //Папка для классов, описывающих CSV схемы обмена данными
      • notice //Папка для классов, описывающих уведомления модуля
      • ... //Здесь может быть создана произвольная структура данных
    • view //Корневая папка для шаблонов отображения
      • hooks //Папка с шаблонами-обработчиками хуков
      • lang //Папка с файлами для превода
        • en //Язык
          • messages.lng.php //Фразы, используемые в PHP, шаблонах
          • messages.js.php //Фразы, используемые в JavaScript
      • ... //Другие языки

Конфигурационные классы модуля

Класс настроек ModuleName\Config\File

Для того, чтобы модуль был идентифицирован в системе и его можно было установить, необходимо объявить в нем класс ModuleName\Config\File. В данном классе должны быть описаны настройки модуля. Значения по-умолчанию этих настроек должны быть описаны в файле module.xml. В дальнейшем, пользователь сможет менять значения настроек модуля в административной панели в разделе Веб-сайт → Настройка модулей

Конфигурационный класс модуля ModuleName::Config::File - должен быть наследником класса RS::Orm::ConfigObject . RS::Orm::ConfigObject - это ORM объект с особым типом хранища, поэтому добавление свойств(настроек модуля) происходит по общим правилам создания свойств у ORM Объектов, через метод _init. Значения настроек по умолчанию должны возвращаться статическим методом getDefaultValues() в виде массива, в котором ключами являются имена свойств. RS::Orm::ConfigObject имеет следующий набор стандартных свойств, описывающих модуль:

Свойство Назначение Ожидаемое значение
name Название модуля String
description Описание String
is_system Флаг, является ли модуль системным. Системные модули невозможно удалить 1 - модуль системный, 0 - модуль обычный
dependfrom Здесь можно указать другие модули, которые должны присутствовать в системе, чтобы установить данный модуль String. названия модулей должны быть перечислены через запятую
version Версия модуля String. Пример версии: 0.1.0.0 - первая цифра - изменения, влияющие на совместимость, вторая - изменение в стуктуре БД, третья - мелкие правки, четвертая - ревизия из системы контроля версий
version_date Дата последнего обновления модуля String
core_version Требуемая для установки версия ядра String. Например: '0.1.0.0' (одна версия), или '0.1.0.0 - 0.2.0.0' (Диапазон версий), или '>=0.1.0.156' или '<=0.1.0.200' (для всех версий младше или старше требуемой), можно указать смешанно, через запятую так: '<=0.1.0.200, 0.2.0.0 - 0.3.0.0, 1.0.0.0, 1.1.0.0'
author Автор модуля String
tools Утилиты по обслуживанию модуля. Отображаются на странице настроек модуля в правой колонке. Array. Например: array(
array(
'url' => '/любая ссылка/',
'title' => t('Название инструмента'),
'description' => t('Описание инструмента')
),
...
)

В ReadyScript имеется подсистема настройки прав доступа к модулям. Права к модулю - это число от 0 до 255. Каждому из 8-ми битов этого числа может быть присвоено название. Администратор может устанавливать флажок для каждого бита в административной части в разделе настройки прав групп пользователей. Назначение каждого бита при этом будет отображено администратору. Чтобы задать описание битам достаточно перегрузить публичное свойство $access_bit в классе конфигурации модуля. В процессе работы модуля можно проверять состояние любого из 8-ми битов перед выполнением того или иного действия.

Пример конфигурационного файла модуля Каталог

<?php
namespace Catalog\Config;
use \RS\Orm\Type;
//Конфигурационный файл модуля Каталог товаров
class File extends \RS\Orm\ConfigObject
{
function _init()
{
parent::_init()->append(array(
'default_cost' => new Type\Integer(array(
'description' => t('Цена по умолчанию'),
'list' => array(array('\Catalog\Model\Costapi', 'staticSelectList'))
)),
'hide_unobtainable_goods' => new Type\Varchar(array(
'description' => t('Скрывать товары с нулевым остатком'),
'listfromarray' => array(array('Y' => 'Да', 'N' => 'Нет')),
'attr' => array(array('size' => 1))
)),
'list_page_size' => new Type\Integer(array(
'description' => t('Количество товаров на одной странице списка')
)),
'items_on_page' => new Type\Varchar(array(
'description' => t('Количество товаров на странице категории. Укажите через запятую, если нужно предоставить выбор'),
'hint' => t('Например: 12,48,96')
)),
'default_weight' => new Type\Integer(array(
'description' => t('Вес одного товара по-умолчанию, грамм'),
'hint' => t('Данное значение можно переустановить в настройках категории или у самого товара')
)),
'default_unit' => new Type\Integer(array(
'description' => t('Единица измерения по-умолчанию'),
'List' => array(array('\Catalog\Model\UnitApi', 'selectList'))
)),
'concat_dir_meta' => new Type\Integer(array(
'description' => t('Дописывать мета теги категорий к товару'),
'hint' => t('Данная опция имеет значение, когда мета данные заданы у товара.'),
'checkboxView' => array(1,0)
)),
'csv_id_fields' => new Type\ArrayList(array(
'runtime' => false,
'description' => t('Поля для идентификации товара при импорте (удерживая CTRL можно выбрать несколько полей)'),
'hint' => t('Во время импорта данных из CSV файла, система сперва будет обновлять товары, у которых будет совпадение значений по указанным здесь колонкам. В противном случае будет создаваться новый товар'),
'list' => array(array('\Catalog\Model\CsvSchema\Product','getPossibleIdFields')),
'size' => 7,
'attr' => array(array('multiple' => true))
)),
));
}
// Возвращает значения свойств по-умолчанию
public static function getDefaultValues()
{
//В случае перегрузки метода getDefaultValues, обязательно нужно вызывать родительский метод,
//чтобы загрузить основные значения из файла module.xml и добавить специфические (зависимые от вызова функций PHP) здесь.
//Если не предусматривается специфических значений, то метод getDefaultValues() - можно не реализовывать.
parent::getDefaultValues() + array(
'tools' => array(
array(
'url' => \RS\Router\Manager::obj()->getAdminUrl('ajaxCleanProperty', array(), 'catalog-tools'),
'title' => t('Удалить несвязанные характеристики'),
'description' => t('Удаляет характеристики и группы, которые не задействованы в товарах или категориях'),
'confirm' => t('Вы действительно хотите удалить несвязанные характеристики?')
),
array(
'url' => \RS\Router\Manager::obj()->getAdminUrl('ajaxCheckAliases', array(), 'catalog-tools'),
'title' => t('Добавить ЧПУ имена товарам и категориям'),
'description' => t('Добавит символьный идентификатор (методом транслитерации) товарам и категориям, у которых он отсутствует.'),
'confirm' => t('Вы действительно хотите добавить ЧПУ имена товарам и категориям?')
)
)
);
}
}

Файл с параметрами модуля module.xml

Файл должен содержать основные сведения о модуле и значения параметров конфигурации по-умолчанию.

Обязательные секции:

  • name - название модуля
  • description - описание модуля
  • version - версия модуля
  • author - автор модуля

Если у секции присутствует атрибут multilanguage="true", то это означает, что значение секции подлежит переводу на текущий язык пользователя. Задать различные языковые версии можно с помощью языковых файлов модуля.

Присутствие у секции атрибута type="array", будет означать, что параметр содержит списковое значение. Элементы списка допускается задавать вложенной секцией value. У секции value может быть задан необязательный атрибут key, который будет определять ключ ассоциативного массива параметра.

Пример файла module.xml:

1 <?xml version="1.0" encoding="utf-8"?>
2 <config>
3  <defaultValues>
4  <name multilanguage="true">Каталог товаров</name>
5  <description multilanguage="true">Администрирование каталога товаров по рубрикам</description>
6  <version>2.0.0.123</version>
7  <author>ReadyScript lab.</author>
8  <is_system>1</is_system>
9  <dependfrom>photo</dependfrom>
10 
11  <default_cost>0</default_cost>
12  <hide_unobtainable_goods>N</hide_unobtainable_goods>
13  <list_page_size>12</list_page_size>
14  <items_on_page>12,48,96</items_on_page>
15 
16  <default_weight>0</default_weight>
17  <default_unit>0</default_unit>
18  <concat_dir_meta>1</concat_dir_meta>
19 
20  <csv_id_fields type="array">
21  <value>title</value>
22  <value>barcode</value>
23  </csv_id_fields>
24  </defaultValues>
25 </config>

Класс установки ModuleName\Config\Install

Чтобы реализовать собственный или дополнить стандартный алгоритм установки модуля, необходимо добавить ему класс-установщик Install в пространстве имен ModuleName\Config. Класс должен имплементировать интерфейс RS::Module::InstallInterface .

Рассмотрим данный интерфейс

<?php
namespace RS\Module;
// Интерфейс классов инсталяции модулей
interface InstallInterface
{
// Выполняет установку модуля. Вызывается когда модуль уже скопирован в основную папку.
// @return bool
public function install();
// Обновляет модуль, приводит в соответствие базу данных.
// Вызывается, когда текущий модуль уже был установлен раннее.
// @return bool
public function update();
// Устанавливает пункты меню для административной части
// @return bool
public function installAdminMenu();
// Возвращает true в случае, если модуль поддерживает вставку демонстрационных данных, иначе - false
// @return bool
public function canInsertDemoData();
// Устнавливает демонстрационные данные.
// @return bool
public function insertDemoData();
// Возвращает ошибки, возникшие в процессе установки
// @return array of error messages
public function getErrors();
// Выполняется, после того, как были установлены все модули.
// Здесь можно устанавливать настройки, которые связаны с другими модулями.
//
// @param array $options параметры установки
// @return bool
public function deferredAfterInstall($options);
}

Для увеличения скорости разработки, в ReadyScript имеется абстрактный класс RS::Module::AbstractInstall, в котором реализовано стандартное поведение установщика модуля. Стандартное поведение заключается в следующем:

  • Метод install выполняет метод update.
  • В методе update происходит поиск всех ORM объектов, расположенных в папке model/orm (включая вложенные папки). У каждого ORM объекта вызывается метод RS::Orm::AbstractObject::dbUpdate(), который приводит в соответствие структуру БД для данного объекта.
  • метод installAdminMenu - не добавляет пунктов меню в административную панель.
  • метод canInsertDemoData - возвращает false, что означает, что у модуля нет демонстрационных данных.
  • остальные методы содержат пустое тело.

Если данное поведение полностью соответствует требованиям модуля, достаточно определить наследника класса RS::Module::AbstractInstall, с пустым телом.

Пример стандартного установщика модуля:

<?php
namespace ModuleName\Config;
// Класс установки модуля
{}

Класс удаления ModuleName\Config\Uninstall

Чтобы реализовать собственный или дополнить стандартный алгоритм удаления модуля, необходимо реализовать класс деинсталяции. Класс должен имплементировать интерфейс RS::Module::UninstallInterface. Рассмотрим данный интерфейс:

<?php
namespace RS\Module;
// Интерфейс классов деинсталяции модулей
interface UninstallInterface
{
// Подготавливает модуль к последующему удалению с диска. В этом методе должны быть описаны действия по
// - удалению зависимостей от данного модуля,
// - удалению таблиц из БД, если таковые имеются
// - удалению пунктов меню, созданных данным модулем
//
// @return bool возвращает true, если подготовка к удалению прошла успешно, в случае,
// если будет возвращен false - физическое удаление модуля с диска не произойдет
//
public function uninstall();
//
// Должен возвращать ошибки, возникшие в процессе удаления модуля
//
// @return array of error messages
public function getErrors();
}

Аналогично функционалу установки модуля, для разработчиков есть готовый абстрактный класс RS::Module::AbstractUninstall, реализующий стандартное поведение модуля при удалении. Под стандартным поведением понимается:

  • В методе uninstall происходит поиск ORM объектов. У каждого найденного ORM объекта вызывается метод RS::Orm::AbstractObject::dropTable() для удаления таблицы из базы данных

После выполнения метода uninstall класса деинсталяции модуля, ReadyScript удаляет папку с модулем. Для создания стандартного класса деинсталяции будет достаточно определить класс UnInstall, как наследника класса RS::Module::AbstractUninstall с пустым телом.

<?php
namespace ModuleName\Config;
// Класс деинсталяции модуля
class Uninstall extends \RS\Module\AbstractUninstall
{
}

Класс единоразовых корректировок ModuleName\Config\Patches

Для случев, когда при обновлении модуля единожды требуется выполнить какой-либо PHP-код разработчика, в ReadyScript предусмотрен класс Patches. Он обязательно должен являться наследником класса RS::Module::AbstractPatches. В классе должен быть реализован метод init(), возвращающий массив идентификаторов патчей. Для каждого идентификатора должен быть реализован метод с одним из следующих префиксов:

  • beforeUpdate{PATCH_ID}
  • afterUpdate{PATCH_ID}

Методы с префиксом beforeUpdate будут выполнены до начала процедуры обновления модуля, методы с префиксом afterUpdate соответственно после. Метод из класса Patches выполняется только если они не были запущены раннее, таким образом все методы выполняются единожды.

Пример класса Patches.

<?php
namespace Search\Config;
/**
* Патчи к модулю
*/
class Patches extends \RS\Module\AbstractPatches
{
/**
* Возвращает массив имен патчей.
*/
function init()
{
return array(
'20072'
);
}
/**
* Патч для релиза 2.0.0.72 и ниже
* Удаляет дубликаты из таблицы с поисковыми индексами, чтобы установить уникальный индекс
*/
function beforeUpdate20072()
{
//... Код патча. Будет выполнен единожды в системе.
}
}

Класс обработки событий ModuleName\Config\Handlers

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

Подписка на события системы и непосредственно сами обработчики событий должны располагаться в классе Handlers, в пространстве имен ModuleName. Класс Handlers должен быть наследником класса RS::Event::HandlerAbstract.

<?php
namespace RS\Event;
// Абстрактный класс обработчиков
abstract class HandlerAbstract
{
// Здесь должна происходить подписка на события
abstract public function init();
// Подписывает обработчик на событие. Сокращенный синтаксис
//
// @param string $event Событие
// @param callback | object | string $callback_class - Имя класса обработчика события или callback для вызова,
// если null, то подставляется $this
// @param string $callback_method Имя статического метода класса обработчика события
// @param integer $priority Приоритет выполнения события, чем выше, тем раньше будет выполнен обработчик
// @return HandlerAbstract
function bind($event, $callback_class = null, $callback_method = null, $priority = 10)
{
if ($callback_class === null) {
$callback_class = $this;
}
\RS\Event\Manager::bind($event, $callback_class, $callback_method, $priority);
return $this;
}
}

В момент инициализации системы событий, скрипт проверяет наличие класса Handlers в каждом модуле и вызывает метод init. Предполагается, что в данном методе произойдет установка обработчиков на события, которые модуль пожелает обрабатывать.

Рассмотрим класс Handlers модуля Shop

<?php
namespace Shop\Config;
use \RS\Router;
class Handlers extends \RS\Event\HandlerAbstract
{
function init()
{
$this
->bind('getroute')
->bind('user.auth')
->bind('delivery.gettypes')
->bind('payment.gettypes')
->bind('printform.getlist')
->bind('orm.init.catalog-product')
->bind('orm.init.catalog-dir');
if (\Setup::$INSTALLED) { //Если движок уже установлен
$this->bind('orm.afterwrite.site-site', $this, 'onSiteCreate');
}
}
// Возвращает маршруты данного модуля
public static function getRoute(array $routes)
{
$routes[] = new Router\Route('shop-front-cartpage', '/cart/', null, t('Корзина'));
$routes[] = new Router\Route('shop-front-checkout', array('/checkout/{Act}/', '/checkout/'), null, t('Оформление заказа'));
//... тело обработчика
return $routes;
}
// Привязывает корзину к пользователю после авторизации
public static function userAuth($params)
{
//... тело обработчика
}
// Возвращает процессоры(типы) доставки, присутствующие в текущем модуле
public static function deliveryGetTypes($list)
{
$list[] = new \Shop\Model\DeliveryType\FixedPay();
//...
return $list;
}
// Возвращает способы оплаты, присутствующие в текущем модуле
public static function paymentGetTypes($list)
{
$list[] = new \Shop\Model\PaymentType\Cash();
// ...
return $list;
}
// Обрабатывает событие - создание сайта
public static function onSiteCreate($params)
{
//... тело обработчика
}
// Добавляем раздел "Налоги" в карточку товара
public static function ormInitCatalogProduct(\Catalog\Model\Orm\Product $orm_product)
{
//... тело обработчика
}
// Добавляем раздел "Налоги" в категорию товара
public static function ormInitCatalogDir(\Catalog\Model\Orm\Dir $orm_dir)
{
//... тело обработчика
}
// Добавляет в систему печатные формы для заказа
public static function printFormGetList($list)
{
//... тело обработчика
}
}

Из приведенного выше примера класса видно, что если у метода bind не указаны параметры $callback_class, $callback_method, то ожидается, что в классе будет присутствовать метод, имя которого соответствует имени события без символов точек и минусов. Метод должен быть публичным и статическим. Такой метод и будет обрабатывать соответствующее событие.

Идея расположить все обработчики событий в одном файле приследует цель, создать единое место, в котором будет видна вся "внешняя" деятельность модуля.

Список всех событий см. в разделе События

Класс прав модуля ModuleName\Config\ModuleRights

По умолчанию для определения доступных в модуле прав и их автоматических проверок используется класс RS::AccessControl::DefaultModuleRights . Чтобы реализовать собственную или дополнить стандартную схему прав, необходимо создать класс прав модуля ModuleRights в пространстве имен ModuleName\Config. Класс должен быть наследником класса RS::AccessControl::DefaultModuleRights .

В классе прав модуля можно перегрузить 2 метода:

  • getSelfModuleRights - возвращает древовидный список собственных прав модуля
    /**
    * @return (\RS\AccessControl\Right|\RS\AccessControl\RightGroup)[]
    */
    protected function getSelfModuleRights()
    {
    return array(
    new \RS\AccessControl\Right('right_id_1', t('Наименование права 1')),
    ...
    new \RS\AccessControl\RightGroup('group_id', t('Наименование группы'), array(
    new \RS\AccessControl\Right('right_id_2', t('Наименование права 2')),
    ...
    )),
    ...
    );
    }
    Заметки
    Рекомендовано указывать идентификаторы прав при помощи констант, для удобства последующих проверок
  • getSelfAutoCheckers - возвращает список собственных инструкций для автоматических проверок прав
    /**
    * @return \RS\AccessControl\AutoCheckers\AutoCheckerInterface[]
    */
    protected function getSelfAutoCheckers()
    {
    return array(
    new \RS\AccessControl\AutoCheckers\ControllerChecker($controller_mask, $method, $action, $request_params, $right, $ignore_missing_rights),
    ...
    );
    }

Классы автоматических проверок прав

В некоторых случаях вместо ручной установки проверки прав в код модуля можно использовать инструкции для автоматических проверок прав

  • \RS\AccessControl\AutoCheckers\ControllerChecker - срабатывает при запуске любого контроллера, проверяет права только в случае соответствия всех указаных параметров параметрам запроса
    • Аргументы конструктора:
      • string $controller_mask - регулярное выражение описывающее имя контроллера, значение '' применяет проверку ко всем контроллерам
      • string|string[] $method - список методов HTTP, значение '*' применяет проверку для любого метода
      • string|string[] $action - список действий контроллера, значение '*' применяет проверку для любого действия
      • array $request_params - параметры запроса переданные в GET, POST, COOKIES
      • string $right - идентификатор проверяемого права
      • bool $ignore_missing_rights - не считать ошибкой отсутствие в модуле проверяемого права (прав из )
    • Примеры использования:
      // всегда проверяет право на чтение
      new \RS\AccessControl\AutoCheckers\ControllerChecker('', '*', '*', array(), 'read'),
      // проверяет персональное право на чтение при запуске соответствующего контроллера
      new \RS\AccessControl\AutoCheckers\ControllerChecker('module-admin-entityctrl', '*', '*', array(), 'entity_read'),
      // проверяет право на запуск определённого действия при post запросе
      new \RS\AccessControl\AutoCheckers\ControllerChecker('module-admin-entityctrl', 'post', 'some_action', array(), 'entity_some_action'),
      // проверяет право на запуск определённого действия при наличии определённого параметра в $_GET
      new \RS\AccessControl\AutoCheckers\ControllerChecker('module-admin-entityctrl', array('post', 'get), '*', array(
      GET => array(
      recalc_something = 1
      ),
      ), 'entity_recalc_something'),

Пример - "файл прав модуля по умолчанию":

<?php
namespace RS\AccessControl;
/**
* Объект прав модуля по умолчанию
*/
class DefaultModuleRights extends AbstractModuleRights
{
const
RIGHT_READ = 'read',
RIGHT_CREATE = 'create',
RIGHT_UPDATE = 'update',
RIGHT_DELETE = 'delete';
/**
* Возвращает собственные права
*
* @return (Right|RightGroup)[]
*/
protected function getSelfModuleRights()
{
return array(
new Right(self::RIGHT_READ, t('Чтение')),
new Right(self::RIGHT_CREATE, t('Создание')),
new Right(self::RIGHT_UPDATE, t('Изменение')),
new Right(self::RIGHT_DELETE, t('Удаление')),
);
}
/**
* Возвращает собственные инструкции для автоматических проверок
*
* @return \RS\AccessControl\AutoCheckers\AutoCheckerInterface[]
*/
protected function getSelfAutoCheckers()
{
return array(
new ControllerChecker('', '*', '*', array(), self::RIGHT_READ, true),
);
}
}

Расширение схемы прав

Список прав модуля и список интрукций для автоматических проверок можно дополнить из стороннего модуля при помощи событий