Как стать автором
Обновить
0

Расширяем функциональность платформы для интернет-магазинов ReadyScript

Время на прочтение10 мин
Количество просмотров5.3K
Сегодня каждый заказчик желает видеть в своем интернет-магазине уникальные фишки. Это могут быть интересные накопительные скидки, реферальные программы, нестандартные фильтры для поиска определенных товаров, и т.д. Все это требует индивидуальных доработок функциональности CMS. В этой статье мы расскажем, какие возможности предлагает ReadyScript для создания индивидуальных решений.



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

1. Обработка событий


Система событий – инструмент, который позволяет изменять стандартный ход работы системы путем установки обработчиков для различных событий системы. Результат работы обработчиков может учитываться системой и корректировать общий ход выполнения программы. Рассмотрим техническую часть событий:

Как только скрипт точки входа index.php получает управление, он ищет у каждого модуля класс Config/MyHandlers или Config/Handlers. Если один из этих классов найден, то у него вызывается метод init, в котором каждый модуль должен подписаться на события. Все обработчики событий также принято описывать в том же классе, в котором происходит подписка. Таким образом, у модуля есть единое место, в котором видна вся его «внешняя» деятельность.

После формирования списка подписчиков, скрипт продолжает свое выполнение, в ходе которого он генерирует различные события, которые обрабатываются модулями.

Вызов событий в ReadyScript происходит в ключевых точках системы, имена событий, как правило, состоят из префикса и изменяемой части. Изменяемая часть обычно несет в себе уточняющий смысл события. Например, все операции записи в базу данных в ReadyScript происходят через ORM объекты, соответственно, в системе есть событие, которое вызывается перед записью данных ORM объекта. Имя таких событий формируется следующим образом:

orm.beforewrite.<короткое имя ORM объекта>, где:
  • короткое имя ORM объекта – формируется из полного имени класса путем нехитрой трансформации. Исключается часть Model\Orm, а слеши заменяются на минусы. Например, если имя класса ORM объекта можно записать так: Catalog\Model\Orm\Product, то его «короткое имя» будет выглядеть так: catalog-product

В данное событие поступают параметры в виде массива, ключами которого являются:
  • orm – объект, который записывается в базу
  • flag – тип сохранения объекта. может принимать значения: INSERT, UPDATE, REPLACE

Если внутри обработчика вызвана остановка выполнения события, то сохранение объекта не будет произведено. Напишем пример кода, который будет останавливать обновление товара, и отображать ошибку: “Не хочу обновлять товар!”.

Для начала потребуется создать простейший модуль со следующей структурой:

test //Корневая папка модуля
    config //Папка для конфигурационных классов
        file.inc.php //Файл конфигурации модуля
        handlers.inc.php //Файл обработчиков событий


Файл file.inc.php может быть следующего содержания:

<?php
namespace Test\Config;
/**
* Конфигурационный файл модуля
*/
class File extends \RS\Orm\ConfigObject
{
    /**
    * Возвращает значения свойств по-умолчанию
    * @return array
    */
    public static function getDefaultValues()
    {
        return array(
            'name' => 'Тест',
            'description' => 'Тестовый модуль',
            'version' => '1.0.0.0',
            'author' => 'ReadyScript lab.'
        );
    }      
}


Модуль должен быть размещен в папке /modules, установлен в системе и включен в разделе Веб-сайт ->Настройка модулей.
Пример файла handlers.inc.php, который перехватывает событие сохранения товара:

<?php
/**
* Данный файл принадлежит модулю Test. 
* Полный путь к текущему файлу /modules/test/config/handlers.inc.php
*/
namespace Test\Config;
 
class Handlers extends \RS\Event\HandlerAbstract
{
    function init()
    {
        $this->bind('orm.beforewrite.catalog-product');
    }
     
    /**
    * Обработчик события "сохранение товара".
    * Имя метода обработчика должно соответствовать событию. 
    * В имени вырезаются все недопустимые для имен функций символы(точки и минусы).
    * Обработчик вызывается без инстанса класса Handlers, поэтому он должен быть статическим.
    * 
    * @param mixed $params - параметры события
    * @param \RS\Event\Event $event - объект текущего события
    * @return void
    */
    public static function ormBeforewriteCatalogProduct(array $params, \RS\Event\Event $event)
    {
        if ($params['flag'] == \RS\Orm\AbstractObject::UPDATE_FLAG) { //Если это обновление товара
            /**
            * Получаем из параметра ORM объект
            * @var \Catalog\Model\Orm\Product
            */
            $product = $params['orm'];
            $product->addError('Не хочу обновлять товар!');
            $event->stopPropagation();
        }
    }    
}


Такой модуль заблокирует возможность сохранения товара во всей системе. Аналогичным образом можно перехватывать события сохранения заказа, пользователя и всех остальных объектов в системе, так как все они являются ORM-объектами.

Возможно ли с помощью событий расширить, например, страницу карточки товара в административной панели и добавить колонку в таблицу товаров в БД?”. Ответ: возможно. Ниже я расскажу как.

Так как все взаимодействие с базой в системе происходит через ORM объекты, будет достаточно установить обработчик на событие инициализации ORM объекта, а в обработчике добавить объекту дополнительные свойства. Напомню, что все свойства объектов задаются во время их первичной инициализации. Инициализация одного класса объектов происходит только один раз за время выполнения PHP скрипта.

Название события инициализации ORM объекта строится следующим образом:
orm.init.<короткое имя ORM объекта>

в качестве параметра в событие поступает инициализируемый ORM объект. Пример кода обработчика, расширяющего карточку товара:
namespace Test\Config;
 
class Handlers extends \RS\Event\HandlerAbstract
{
    function init()
    {
        $this->bind('orm.init.catalog-product');
    }
     
    /**
    * Обработчик события "Инициализация ORM объекта Товар".
    * Не забудьте переустановить модуль Каталог через меню Веб-сайт->Настройка модулей. Каталог товаров -> переустановить
    *
    * @param \Catalog\Model\Orm\Product
    * @return void
    */
    public static function ormInitCatalogProduct(\Catalog\Model\Orm\Product $orm_product)
    {
        $orm_product->getPropertyIterator()->append(array( //Добавляем свойства к объекту
            'Новая закладка', //Закладка. Появится в форме редактирования товара
             
                'test_property' => new \RS\Orm\Type\Integer(array( //Тип поля. Задает тип в базе INT
                    'maxLength' => 1, // Длина поля в базе будет INT(1)
                    'description' => 'Тестовый флаг', //Название поля
                    'checkboxView' => array(1,0), //1 - значение отмеченного checkbox, 0 - для неотмеченного
                    //также здесь можно:
                    //- задавать произвольный шаблон для отображения данного свойства. см. методы \RS\Orm\Type\Integer
                ))
        ));
    }
}


После добавления такого обработчика в систему, нужно переустановить модуль «каталог товаров» (через административную панель, раздел Веб-сайт->Настройка модулей->Каталог товаров), чтобы ReadyScript добавил новую колонку в таблицу БД объекта \Catalog\Model\Orm\Product.

В карточке товара новое поле будет располагаться на отдельной закладке и выглядеть так:



Рассмотрим еще один пример возможностей системы событий. Предположим, что возникла задача добавить пункт в меню действий над одним товаром, которое располагается в разделе «каталог товаров» в административной панели. Для решения задачи потребуется перехватить событие, которое бросает CRUD-контроллер(\RS\Controller\Admin\Crud) после формирования хелпера внешнего вида и перед вызовом действия(action) контроллера. Название события:

controller.exec.<короткое имя контроллера>.<имя действия>, где
  • короткое имя контроллера — формируется путем исключения части \Controller из полного имени класса, включая NameSpace. И замены обратных слешей на минус.
  • имя действия – название действия, которое будет вызвано у контроллера(без префикса action)

Например, если вызывается контроллер \Catalog\Controller\Admin\Ctrl с действием Index, то полное название события будет выглядеть так: controller.exec.catalog-admin-ctrl.index

Ниже приведен пример обработчика этого события:

<?php
namespace Test\Config;
class Handlers extends \RS\Event\HandlerAbstract
{
    function init()
    {
        $this->bind('controller.exec.catalog-admin-ctrl.index');
    }
     
    /**
    * Обработчик события
    *
    * @param \RS\Controller\Admin\Helper\CrudCollection $helper - Хелпер визульной части админ. панели
    * @return void
    */
    public static function controllerExecCatalogAdminCtrlIndex(\RS\Controller\Admin\Helper\CrudCollection $helper)
    {
        /**
        * @var \RS\Html\Table\Control - объект: Менеджер таблицы
        */
        $table_control = $helper['table'];
        $columns = $table_control->getTable()->getColumns(); //Получаем колонки таблицы
        foreach($columns as $column) {
            if ($column instanceof \RS\Html\Table\Type\Actions) { //Ищем колонку с действиями
                //в $column - колонка с инструментами для работы с одной записью
                //перебираем инструменты и находим выпадающий список
                foreach($column->getActions() as $action) {
                    if ($action instanceof \RS\Html\Table\Type\Action\DropDown) {
                        //Добавляем свой пункт в выпадающий список действий над
                        //одним товаром в разделе Каталог Товаров в админ. панели
                        $action->addItem(array(
                            'title' => 'ТЕСТОВОЕ ДЕЙСТВИЕ!', //текст ссылки
                            'attr' => array( //атрибуты ссылки
                                //воспользуемся микроформатом crud-get, который выполнит ссылку через ajax и перезагрузит страницу
                                'class' => 'crud-get',
                                //crud-get будет использовать атрибут data-confirm-text, чтобы выдать диалог подтверждения, перед выполнением операции
                                'data-confirm-text' => 'Вы действтельно хотите выполнить ТЕСТОВОЕ ДЕЙСТВИЕ(безопасно)?',
                                //@ перед именем атрибута, означает что это динамический атрибут, т.е. он будет разный для каждой строки
                                //в значение динамического атрибута будут подставляться значения полей из объекта строки.
                                //например, в такой строке: ?id=@id&do=action, будет заменен @id, на значение поля id объекта строки
                                //В данном примере getAdminPattern сформирует следующую ссылку /admin/test-example/?do=Action&id=@id
                                '@href' => \RS\Router\Manager::obj()->getAdminPattern('Action', array(':id' => '@id'), 'test-example'),
                            ),
                        ));
                    }
                }
            }
        }
    }
}


Результатом действия этого кода, будет следующий пункт в меню:



Полный список событий в ReadyScript с описаниями можно найти в официальной документации здесь.

2. Перегрузка маршрутов


Перегрузка маршрутов позволяет передать определенные адреса (URL) сайта на обработку неродному контроллеру.
Допустим, у нас возникла потребность внести изменения в контроллер оформления заказа. Как это лучше сделать?

Чтобы ответить на вопрос, рассмотрим ситуацию целиком. Управление контроллеру передает маршрут. Именно он устанавливает связь между URL /checkout/… и контроллером \Shop\Controller\Front\Checkout. Другие модули и шаблоны используют id этого маршрута(shop-front-checkout) для формирования ссылок на оформление заказа. Не спроста id маршрута является коротким именем контроллера, так как если у маршрута явно не задан контроллер-обработчик, которому будет переходить управление, то ищется контроллер, короткое имя которого равно id маршрута.

Соответственно, решением нашей задачи могло бы стать создание маршрута с тем же id, обрабатывающим те же URL, но который будет передавать управление другому контроллеру. А уже в другом контроллере мы можем реализовать все что угодно.

Возникает задача — отключить регистрируемый чужим модулем маршрут и создать маршрут с таким же id своим модулем. В этом нам поможет возможность подсистемы событий задавать приоритет обработчику событий.

Так как все маршруты вводятся в систему с помощью события getroute, достаточно установить в нашем модуле такой приоритет обработчика, чтобы он вызвался в самом конце. Тут уже сработает правило: при добавлении двух маршрутов с одинаковым id, в системе остается последний. Рассмотрим код:

<?php
namespace Test\Config;
 
class Handlers extends \RS\Event\HandlerAbstract
{
    function init()
    {
        $this->bind('getroute',
                    null, //callback_class - по умолчанию $this
                    null, //callback_method - по умолчанию формируется из названия события
                    0 //Приоритет. Чем ниже приоритет, тем позже выполняется событие. Приоритет по-умолчанию - 10
        );
    }
     
    /**
    * Возвращает маршруты данного модуля
    */
    public static function getRoute(array $routes)
    {       
        //Добавляем маршрут в систему
        $routes[] = new \RS\Router\Route(
            'shop-front-checkout', //ID маршрута
            array('/checkout/{Act}/', '/checkout/'), //Перечень обрабатываемых URL
            array('controller' => 'test-front-examplecontroller'),  //Параметры по-умолчанию. Направляем запросы на другой контроллер
            'Оформление заказа' //Название маршрута для админ. панели
        );
         
        return $routes;
    }
}


Приведенный выше обработчик направит все запросы на контроллер с классом \Test\Controller\Front\ExampleController, который уже по желанию разработчика можно унаследовать от \Shop\Controller\Front\Checkout, и перегрузить какие-то избранные методы, либо реализовать контроллер полностью с нуля.

3. Замена стандартных обработчиков событий


Если есть несогласие с тем, как тот или иной модуль обрабатывает события, ReadyScript позволяет перегрузить «чужой» файл обработки событий. Как уже было сказано выше, во время инициализации системы событий, у модулей ищется сперва класс MyHandlers, а затем Handlers, чтобы вызвать у первого найденного метод init.

Достаточно задекларировать у чужого модуля в пространстве имен \ИМЯ_МОДУЛЯ\Config класс MyHandlers, чтобы указать в нем собственные обработчики событий.

Если не требуется глобальных изменений, можно создать класс MyHandlers, унаследованный от Handlers и перегрузить только некоторые методы.

4. Подмена классов (НЕ рекомендуемый способ)


Вся функциональность в ReadyScript реализована в классах. Все классы модулей и ядра подгружаются с помощью autoload’а. В правилах подключения скриптов заложено следующее условие: сперва в папке, соответствующей namespace’у класса ищутся файлы ИМЯ КЛАССА.my.inc.php и если такого не существует, то производится поиск и подключение файла ИМЯ КЛАССА.inc.php

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

Тем не менее, данный способ является НЕ рекомендуемым, но о нем должны знать разработчики. При обновлении в «оригинальных» классах могут изменяться методы, которые могут использовать другие модули, что в целом может сказаться на стабильности системы.

5. Возможности тем оформления


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

За внешний вид сайта отвечает тема оформления. В теме оформления заложена возможность перегружать дефолтные шаблоны модулей, для этого служит папка moduleview. Чтобы перегрузить шаблон карточки товара /modules/catalog/view/product.tpl достаточно скопировать его по следующему пути /templates/{ВАША_ТЕМА_ОФОРМЛЕНИЯ}/moduleview/catalog/product.tpl, после чего можно править новый файл. Стоит заметить, что все CSS и JS файлы подключаются в шаблонах, это позволяет перегрузить необходимый шаблон и прописать в нем новые конструкции подключения стилей и скриптов уже по новым путям.

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

Заключение


Возможности платформы ReadyScript позволяют разработчикам воплощать самые изощренные задумки заказчиков. Архитектура системы подталкивает к расширению функциональности системы при помощи дополнительных модулей, что в последствии позволяет повторно использовать написанный код.
Теги:
Хабы:
Всего голосов 16: ↑11 и ↓5+6
Комментарии2

Публикации

Информация

Сайт
readyscript.ru
Дата регистрации
Дата основания
Численность
2–10 человек
Местоположение
Россия

Истории