Перво-наперво, в основе IPS лежит парадигма MVC. Структура каталогов следующая:
Папка system — здесь находятся все классы и библиотеки фреймворка.
Если вы видите в коде строку типа \IPS\Db::i()->select(... - это значит, что автозагрузчик классов будет смотреть в директорию system/Db в поисках файла Db.php , в котором должен быть определен класс _Db (с нижним подчеркиванием) в пространстве имен IPS (namespace IPS;).
В случае, когда класс находится не в одноименном файле, как, например, реализация итератора на основе SELECT-запроса из БД (файл system/Db/Select.php), обращение к нему имеет вид \IPS\Db\Select. Пространство имен в файле данного класса - IPS\Db (namespace IPS\Db;).
Кстати, метод select класса Db как раз и возвращает объект класса \IPS\Db\Select.
Статические методы вида ::i() создают (при необходимости) и возвращают экземпляр класса. Экземпляры класса при создании помещаются в хранилище мультитонов (в свойства-массивы $multitons или $instances), если, конечно, класс не потомок синглетона, откуда впоследствии и достаются при повторном обращении к ::i(). В аргументах к этому методу при необходимости можно передать идентификатор для экземпляра.
Папка applications — содержит классы и библиотеки всех приложений, в т.ч. приложение Ядро (core) и Форумы (forums).
Ключевые поддиректории папки любого приложения — это:
папка sources — здесь хранятся классы моделей приложения, часто расширяющие классы моделей фреймворка. Названия папок, файлов и самих классов начинаются с большой буквы.
Возьмем модель Post. Класс модели лежит в applications/forums/sources/Topic/Post.php
У этого класса пространство имен IPS\forums\Topic. Чтобы обратиться из другого класса, например, к статическому методу create, нужна такая конструкция \IPS\forums\Topic\Post::create(...
В отличие от первого примера с Db здесь между IPS и папкой моделей Topic указывается еще и папка приложения - forums. Если забыть про нее, автозагрузчик полезет в папку system и, не найдя требуемого класса, вернет ошибку.
Обращение к классу модели из одноименной папки будет иметь вид \IPS\forums\Topic.
Чтобы не ошибиться, смотрим на директиву namespace в файле класса, к которому хотим обратиться, предваряем ее значение обратным слэшем (\IPS\forums , а не просто IPS\forums) и добавляем в конец имя класса через обратный слэш (\) без знака подчеркивания.
Папка modules — содержит классы контроллеров модулей, как для фронтэнда (подпапка front), так и бэкэнда (подпапка admin).
Рассмотрим страницу админки Groups в меню Members. Url у нее такой
/admin/?adsess=89q83okvlprm8t6jn1cm7e06n4&app=core&module=members&controller=groups
Параметр controller указывает диспетчеру бэкэнда, что нужно загрузить класс groups модуля members (параметр module), которые принадлежат приложению core (параметр app). Если указан еще параметр do, контроллер обычно выполняет метод, указанный в do, либо, когда do не указан, метод по-умолчанию — manage(). К примеру, для страницы редактирования группы Url выглядит так
/admin/?adsess=89q83okvlprm8t6jn1cm7e06n4&app=core&module=members&controller=groups&do=form&id=4
Контроллер выполнит метод form() , если таковой существует в классе groups или вернет ошибку 404.
Папка data — содержит всевозможные файлы данных (json, xml), которые могут пригодиться, например, на этапе установки приложения, а также файлы шаблонов phtml.
В папке hooks лежат классы приложения, меняющие логику работы классов фреймворка.
В папке setup — установщики и апгрейдеры.
В папке interface лежат как сторонние js-библиотеки, так и прочие функции, используемые в контексте взаимодействия с приложением. Логика работы, например, шлюзов данного приложения. К шлюзам, идут ответы извне, например, от платежной системы или системы внешей авторизации, поэтому они представляют собой что-то вроде корневого index-файла.
Папка tasks содержит классы моделей - потомков модели \IPS\Task фреймворка , реализующих исполнение задач (обслуживания), специфических для данного приложения: удаление более не нужных файлов, очистка БД и т. д.
Вернемся в корень IPS.
Папка uploads хранит логи, минимизированные файлы javascript и css, картинки и загрузки.
В папке datastore лежат кэши. Ее можно сменить в админке.
В папке admin лежат входные скрипты бэкэнда и апгрейда. В папке api тоже только входной скрипт.
Алгоритм работы движка рассмотрим на примере фронтэнда. В конце каждого пункта, кроме первого, приведен класс::метод(), в котором выполняется обозначенная работа.
1. Входной скрипт подключает файл инициализации init.php (описание базового класса IPS, куда входят константы, обработчики ошибок, автозагрузчик классов и т.д.) и запускает диспетчера фронтэнда - \IPS\Dispatcher\Front::i()->run().
2. Фронт-диспетчер проверяет, создан ли файл конфигурации. Если это не подтверждается, идет редирект на установку. \IPS\Dispatcher::i()
3. Далее проверяется, есть ли кэш страницы, и если есть — он идет на вывод. \IPS\Dispatcher\Front::init()
4. Если нет или истек, работа продолжается. В dev-режиме выполняется синхронизация данных разработки. \IPS\Dispatcher\Front::init()
5. Идет подключение базовых css и js-файлов к выводу. \IPS\Dispatcher\Front::init()
6. Далее идет обработка ЧПУ и присвоение свойств синглетону \IPS\Request. \IPS\Dispatcher\Front::init()
7. Принудительное использование https. \IPS\Dispatcher\Standart::init()
8. Установка локали пользователя. \IPS\Dispatcher\Standart::init()
9. Загрузка из БД данных о текущем приложении и создание объекта приложения на их основе.Если в \IPS\Request не указано приложение (параметр app) или его не получить из БД, загружаются все приложения и выделяется выбранное в админке по-умолчанию. Если и такого нет, то первое в коллекции. Список всех приложений из БД хранится в кэше. \IPS\Dispatcher\Standart::init()
10. Дальше идет инициализация выбранного приложения (подготовительные операции). \IPS\Dispatcher\Standart::init()
11. Выбирается модуль приложения для работы, если он указан в запросе. Логика аналогична выбору приложения. \IPS\Dispatcher\Standart::init()
12. Определяется контроллер модуля на основании запроса либо тот, что назначен модулю по-умолчанию. \IPS\Dispatcher\Standart::init()
13. На основании данных о приложении, области (фронтэнд или бэкэнд), идентификатора модуля и имени контроллера формируется полное имя класса контроллера для автозагрузчика. Поскольку в данном случае у нас фронт-диспетчер, его свойство controllerLocation = 'front' , т. е. область у нас фронтэнда. Напротив, входной скрипт админки запускает бэкэнд-диспетчера и область у него 'admin'. \IPS\Dispatcher\Standart::init()
14. Загружаются в память шаблоны, подключаются к выводу css и js-файлы активного приложения. \IPS\Dispatcher\Standart::init()
15. Включается сайдбар, который может быть выключен контроллерами, проверяется пользователь на бан и на достаточность информации о нем (если он только зарегистрировался). Проверяются права доступа к текущему модулю. \IPS\Dispatcher\Front::init()
16. Дальше идут всякие штуки для вывода: формирование базовой навигации (крошки), параметров поиска (предусмотрена ли в данном приложении функция глобального поиска). \IPS\Dispatcher\Front::init()
17. Теперь уже в память загружается экземпляр класса, определенного в п.13. Это контроллер активного модуля и он должен быть потомком \IPS\Dispatcher\Controller. Далее он выполняется методом execute(). \IPS\Dispatcher::i()->run()
18. После того как контроллер выполнил свою работу, диспетчер запускает уже свой метод finish(). \IPS\Dispatcher::i()->run()
19. В нем подключаются к выводу виджеты сайдбара. Формируются метатеги. \IPS\Dispatcher\Front::i()->finish()
20. Подключаются к выводу js-модели фреймворка javascript. \IPS\Dispatcher\Standart::finish()
21. На заключительном этапе метода finish() выводится результат посредством \IPS\Output::i()->sendOutput(... Образ для вывода формирует система тем и шаблонов. \IPS\Theme::i()->getTemplate(... В зависимости от того идет ajax-запрос или нет, на вывод пойдет пустой шаблон (blankTemplate) с ajax-данными или общий шаблон (globalTemplate) соответственно. \IPS\Dispatcher::finish();
22. По окончании деструктор фронт-диспетчера запускает исполнение очереди задач обслуживания. \IPS\Dispatcher\Standart::__desctruct()
Во многих местах идет проверка запроса на ajax - if ( !\IPS\Request::i()->isAjax() ).... От этого зависит логика вывода шаблонов, css и js.
Для создания и расширении функционала IPS полезно изучить классы папки system в первую очередь.
Например:
В system/Helpers/Form лежат описания различных типов полей ввода для форм — чекбоксы, диапазоны, селекты, ссылочные поля и т. д. Все они расширяют базовый абстрактный класс FormAbstract в той же папке. За непосредственное построение форм отвечает класс Form, лежащий рядом.
В папке system/Patterns/ лежат шаблоны проектирования — ActiveRecord, Singleton, итераторы.
В отличии от разработки приложений под IPS, разработка плагинов обычно представляет собой расширение или изменение имеющегося функционала, а не добавление новых фундаментальных возможностей. Плагины состоят из хуков кода (Code Hook) и/или хуков шаблонов (Theme Hook). Code Hook — это потомок какого-либо класса фреймворка или приложения, содержащий, как правило, модифицированные методы родителя. Theme Hook — модифицирует и заменяет блоки верстки в имеющихся шаблонах.
В основе js-фреймворка IPS также лежит парадигма MVC. Далее я буду называть его ips. Итак, ips работает на jQuery и активно задействует библиотеку Underscore.js (или _ ). Исходники фреймворка и глобальных библиотек можно посмотреть в dev-режиме в папке dev/js.
Ядро ips — dev/js/library/app.js В libraries лежат также общие библиотеки под javascript — jquery, js-шаблонизатор mustache, underscore, prettify и проч.
В dev/js/framework/ находятся кирпичики ips.
В подпапке common — реализация базовых модели, контроллера, загрузчика, а также модули пользовательского интерфейса (подпапка ui) и модули утилит (подпапка utils).
В подпапке controllers лежит глобальный контроллер для wysiwyg-редактора.
В подпапке templates — файл с глобальными шаблонами.
В папках dev/js/front и dev/js/admin лежат глобальные контроллеры, модели и шаблоны, специфичные для фронтэнда и бэкенда соответственно.
Модули утилит обеспечивают такой функционал как работа с css, куками, временем, событиями и т. д. Модули интерфейса обеспечивают интерактивность страниц и инициализируются data-атрибутами элементов. Контроллеры — это тоже модули (вообще, большинство кода ips — это автономные модули), реагирующие на события элементов интерфейса (UI-виджетов) или на другие контроллеры и выполняющие соответствующие действия.
В приложениях для IPS также могут быть свои js-ресурсы. Они лежат в applications/<приложение>/dev/js . Подпапки admin, front, global содержат контроллеры, шаблоны и примеси (mixins — расширяют функционал контроллеров) js конткретного приложения для бэкенда, фронтэнда и общего использования соответственно.
ips — это событийный фреймворк. Контроллеры ips откликаются именно на события, которые генерируются UI-виджетами (это ui-модули). Чтобы отработал тот или иной контроллер, добавляющий интерактивность странице, необходимо, чтобы виджет вызывал какие-либо события. Сами контроллеры друг с другом не взаимодействуют. События обычно вызывают на элементе, к которому присоединен виджет:
$( elem ).trigger( 'myWidgetEvent', { color: 'red', size: 'large' } );
Модули виджетов определяются в пространстве имен ips.ui Возьмем стандартный пример виджета, скрывающий элемент при клике:
;( function($, _, undefined){
"use strict";
ips.createModule('ips.ui.hideElem', function(){
var respond = function (elem, options, e) {
if( options.animate ){
$( elem ).fadeOut();
} else {
$( elem ).hide();
}
$( elem ).trigger( 'hiddenElement' );
};
ips.ui.registerWidget( 'hideElem', ips.ui.hideElem, [ 'animate' ], { lazyLoad: true, lazyEvent: 'click' );
return {
respond: respond
};
});
}(jQuery, _));
Экземпляры виджета создаются модулем на определенных узлах DOM. Метод createModule описывает функционал виджета, который обязан содержать метод отклика - либо стандартный respond, либо другой (callback), который будет вызван при регистрации виджета методом registerWidget.
Метод registerWidget принимает ключ виджета ('hideElem'), ссылку на модуль-обработчик виджета (ips.ui.hideElem), список принимаемых data-параметров для инициализации ('animate'), способ инициализации виджета: при загрузке страницы сразу или при действии пользователя — так называемый lazyLoad. В последнем случае, когда lazyLoad: true также указывается событие lazyEvent: 'click' , на котором виджет инициализируется. Также можно передать callback-функцию, заменяющую respond.
Прикрутить виджет к элементу DOM можно так:
<button data-ipsHideElem data-ipsHideElem-animate='true'>Щелкни, чтобы скрыть меня</button>
Параметр animate (на элементе DOM он должен выглядеть как data-ipsHideElem-animate) указывает, должен ли элемент затухать или скрыть его мгновенно. Это определяется в методе respond, в аргументы которого передаются объект, на котором инициализирован виджет, список параметров (animate) и объект события, если виджет инициирован на событие пользователя (щелчок), как в данном случае.
Код контроллера может быть таким:
;( function($, _, undefined){
"use strict";
ips.controller.register('plugins.globalMessageDismiss', {
initialize: function () {
this.on( document, 'click', '[data-action="dismiss"]', this.dismiss );
},
dismiss: function (e) {
e.preventDefault();
var url = $( e.currentTarget ).attr('href');
var message = $(this.scope);
ips.getAjax()(url).done(function(){
ips.utils.anim.go( 'fadeOut', message );
}).fail(function(){
window.location = url;
});
}
});
}(jQuery, _));
Регистрируется новый контроллер в системе методом ips.controller.register. Дальше в кавычках указан его тип (plugin) и имя (globalMessageDismiss). Контроллер должен обязательно содержать метод initialize, в котором перечисляются события, на которые откликается только этот контроллер. Для всех прочих задач лучше объявить еще метод setup и вызвать его в начале или конце initialize.
В initialize указание обработчика события можно сделать с помощью метода this.on , где this — указатель на текущий контроллер. Первым передается ссылка на элемент, события которого будет наблюдаться. Затем идет указание самого события.
Следующим передается селектор для идентификации элемента (делегирование в jQuery), на котором происходит событие. Это необязательный параметр на случай, если первый параметр — контейнер элементов, на которых инициируются события. Последний параметр - callback-функция, собственно, и осуществляющая работу.
Чтобы контроллер заработал, в шаблоне должен быть определен элемент с атрибутом:
data-controller="plugins.globalMessageDismiss"
а также элемент с атрибутом:
data-action="dismiss"
Порядок разработки плагинов для IPS сводится к следующему.
1. Переход в dev-режим (наличие Developer Tools и константа IN_DEV в true) и создание нового плагина в админке (System — Plugins — Create New)
2. Создаем хуки. При этом в папке plugins/<имя_папки_плагина>/hooks будут созданы соответствующие файлы, которые можно редактировать по FTP.
3. Хуки шаблонов модифицируют исходные шаблоны с помощью селекторов (аналогично jQuery, CSS), заменяя оригинальные куски HTML или дополняя их. Предварительно нужно только указать, какой именно шаблон меняется. Все это делается в админке. HTML можно вставлть непосредственно:
<div class="ipsMessage ipsMessage_information">This is the global message.</div>
либо использовать подшаблоны:
{template="globalMessage" group="plugins" location="global" app="core"}
а подшаблон globalMessage.phtml с вышеприведенным div'ом разместить в plugins/<имя_папки_плагина>/dev/html:
<ips:template parameters="" />
<div class="ipsMessage ipsMessage_information">
Now using a template!
</div>
В первой строке можно указать переменные (parameters), которые должны быть переданы в шаблон.
Если нужно включить доп. css-файлы, их размещют в ...dev/css папки плагина, откуда они грузятся автоматом.
4. Специфичные настройки для плагина сначала создаются в админке во вкладке Settings центра разработки (Developer Center). Это резервирует соотв. место в БД, однако, чтобы админ после установки, мог менять настройки, следует добавить соотв. элемент в форму. В папке плагина лежат файл settings.rename.php, который нужно переименовать в settings.php и изменить в нем существующий код для примера на код добавления элемента формы:
$form->add( new \IPS\Helpers\Form\Editor( 'globalMessage_content', \IPS\Settings::i()->globalMessage_content, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => 'globalMessage_content' ) ) );
globalMessage_content — название настройки
\IPS\Settings::i()->globalMessage_content — сохраненное значение или значение по-умолчанию
$form — ссылка на форму для изменения настроек плагина
new \IPS\Helpers\Form\Editor( - создание экземпляра редактора
5. Добавить языковые строки (созданная настройка пока имеет системную метку globalMessage_content) можно в файле ...dev/lang.php
$lang = array(
'globalMessage_content' => "Сообщение",
);
6. Логика работы шаблона теперь может выглядеть так:
<ips:template parameters="" />
{{if settings.globalMessage_content}}
<div class="ipsMessage ipsMessage_information">
{setting="globalMessage_content"}
</div>
{{endif}}
7. Если плагин предусматривает работу с БД и предварительное ее изменение, все подготовительные операции с БД можно поместить на этап инсталляции плагина. Идем в ...dev/setup плагина, открываем install.php и редактируем, например, метод step1:
\IPS\Db::i()->addColumn( 'core_members', array(
'name' => 'globalMessage_dismissed',
'type' => 'BIT',
'length' => 1,
'null' => FALSE,
'default' => 0,
'comment' => 'If 1, the user has dismissed the global message'
) );
return TRUE;
что аналогично sql-запросу:
ALTER TABLE core_members ADD COLUMN globalMessage_dismissed BIT(1) NOT NULL DEFAULT 0 COMMENT 'If 1, the user has dismissed the global message';
8. Создание Code Hook в админке начинается с указания класса, который будет модифицироваться. Чтобы не ошибиться, смотрим на директиву namespace этого класса и добавляем имя класса. Вышиприведенные примеры просто выводят сообщение пользователю, которое он может скрыть. Во время скрытия, мы можем указать, как именно серверная часть это обработает, например, сохранит, что пользователь видел сообщение, как ниже. В качестве класса для расширения подойдет любой контроллер, но можно взять общий для подобных целей \IPS\core\modules\front\system\plugins
public function dismissGlobalMessage()
{
\IPS\Session::i()->csrfCheck();
if ( \IPS\Member::loggedIn()->member_id )
{
\IPS\Member::loggedIn()->globalMessage_dismissed = TRUE;
\IPS\Member::loggedIn()->save();
}
else
{
\IPS\Request::i()->setCookie( 'globalMessage_dismissed', TRUE );
}
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( NULL, 200 );
}
else
{
\IPS\Output::i()->redirect( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : \IPS\Http\Url::internal( '' ) );
}
}
Сначала идет проверка на межсайтовую подделку запроса (раз уж мы задействуем пользователя) — csrfCheck() В случае фэйла, выполнение прервется автоматом. Потом проверяется залогиненность пользователя. \IPS\Member — реализует паттерн ActiveRecord, поэтому дальше идет сохранение данных в базу или в куку (на случай, когда посетитель не залогинен), а после редирект.
9. Конечный шаблон сообщения выглядит так:
<ips:template parameters="" />
{{if settings.globalMessage_content and !member.globalMessage_dismissed and !isset( cookie.globalMessage_dismissed )}}
<div class="ipsMessage ipsMessage_information" data-controller="plugins.globalMessageDismiss">
<a href="{url="app=core&module=system§ion=plugins&do=dismissGlobalMessage" csrf="1"}" class="ipsMessage_code ipsType_blendlinks ipsPos_right" data-action="dismiss"><i class="fa fa-times"></i></a></span>
{setting="globalMessage_content"}
</div>
{{endif}}
10. Пример с реализацией javascript'ового контроллера, который завязан на клик по ссылке с атрибутом data-action="dismiss", скрывает сообщение и обращается к серверной части через ajax, был рассмотрен выше.
11. Скачиваем через админку xml-файл плагина для его последующего распространения.