Pull to refresh

Разработка модульного движка на PHP

Reading time5 min
Views8.8K
Есть много разных движков на PHP, от достаточно простых, до очень тяжеловесных и громоздих, включающих практически все.

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

Меньше полугода назад я задался вопросом создания такого движка. Первым, что нужно было написать, являлся, загрузчик. Эдакий псевдо-модуль (об этом далее), который загружает иные модули.

Давайте определимся со структурой: допустим у нас есть папка mod в которой хранятся модули.
Например /mod/staticpages/*. По стандарту загрузчика модуль должен состоять из конфигурационного файла, главного класса модуля, опционально подключаемых библиотек.

Опять же, например, есть модуль staticpages (который отвечает за обработку статических страниц), он будет состоять из файлов manifest.ini и staticpages.php.
Первый — конфигурация модуля, а второй — файл с главным классом модуля.
Для начала пусть конфиг-файл имеет такую структуру:
[Custom]
name = "Static Pages"; 
author = "ShadowPrince";
devname = "staticpages"; 
version = "0.1"; 

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

Добавим в манифест еще один отдел:
[Module]
mainclass = "staticpages.php"; 
mainclassname = "StaticPages"; 

Теперь при обработке этого модуля мы можем загрузить (точнее включить) файл с его классом и создать его экземпляр.
Сейчас алгоритм будет примерно таким:
1. Читаем все папки /mod/.
2. Если там есть manifest.ini, продолжаем.
3. Получаем значение mainclass, включаем этот файл.
4. Создаем экземпляр класса с именем devname. Именно для этого и нужно это поле.
В итоге мы получаем обьект класса StaticPages с именем $staticpages. Обьект будет являтся глобальным, для удобного взаимодействия иных модулей с ним.
Теперь в дальнейшем коде мы можем просто и быстро использовать возможности этого модуля.

Но теперь мы упираемся еще в одну проблему:
Допустим у нас есть такой запрос: "?ins=staticpage&page=info", который, по идее, должен показать статическую страницу с именем info. Но как об этом узнает модуль Static Pages, который и должен отвечать за это?
Конечно, можно разместить обработчик в конструктор класса, типа, если ins = staticpage, но ведь на тот момент мы точно не знаем — загрузились ли иные модули, которые нужны для нормальной работы Static Pages, и вообще — стоит ли это делать?

Значит нам нужно добавить в манифест еще один отдел:
[Run]
run = "template()"

Уже после загрузки всех модулей мы запускаем второй этап: запуск методов.
В этой секции, в параметре run указан метод класа, который нужно запустить на этой стадии. Отлично, на этом этапе уже точно будут загружены все модули, и они могут спокойно взаимодействовать друг с другом.

Теперь еще одна ситуация — допустим модуль pex (который отвечает за права доступа) в этой секции только начинает загружать права доступа для текущего пользователя, а нам ну по зарез нужно ограничить показ статической страницы, а вмешиватся в код стороннего модуля совсем не хорошо.

Придется ввести еще одно понятие, а так же параметр в манифест, а именно: очередь и require.
Очередь, в нашем случае, очередь модулей (что очевидно) для выполнения некоторого действия.
А сейчас — модифицируем отдел [Run] модуля Static Pages:
[Run]
run = "template()"
require = "pex"

При попытке выполнения метода template() для нашего модуля Static Pages загрузчик наткнется на требование сначала выполнить аналогичное действие для модуля pex, подвинет наш модуль подальше в очереди и продолжит аналогичную работу для остальных модулей.
Теперь мы можем спокойно орудовать данными, получеными pex'ом уже после его Run этапа.

Еще одной важная вещь: проверка жестко установленных модулей для работы нашего Static Pages, а так же их версий. Добавим такой отдел:
[Require]
data = "core:0.2, template:0.1, mysql:0:1, lang:0.2";

Видно, что наш модуль требует загрузчик версии не ниже 0.2 (по этому он и является «псевдо-модулем»: он имеет и манифест, и версию, и к нему можно получить доступ как к любому иному модулю, но он «жестко впаян» в систему), так же модуль для работы с БД MySQL, еще модуль lang (который будет отвечать за кодировку, формат даты и времени, и тд.), а так же модуль для постраничной навигации.

Но наши «очереди» не очень то хорошо сказываются на производительности, да и разработчикам будет утомительно указывать все модули, которые тем или иным боком зависят от него. Поэтому найболее разумным будет сделать «пару уровней» выполнения модульных методов.
Это будут новые отделы в манифесте: сначала будут выполнятся методы с [Prev], уже известный [Run], [After] и [Finish].

Для примера возьмем часть манифеста модуля pex:
[Prev]
run = "getGroup()";
require = "auth";
[Run]
run = "template()";
[After]
run = "nodesContainers()"

Сначала, на этапе [Prev] он получает права группы, к которой принадлежит пользователь, но только после того, как модуль auth получит данные о самом пользователе. Потом он выполнит метод template(), в котором, например, проверит, можно ли текущему пользователю просматривать сайт (но и даст иным модулям сделать свою работу).
А уже после — проверит шаблон на так называемые nodesContainers, участки шаблона, для доступа к которым нужные некоторые права (ведь в пред. этапах разные модули могли добавить такие участки и они бы остались не обоработанные).

Так же не забываем про библиотеки, которые могут понадобится некоторым модулям, добавим еще один отдел:
[Include]
dirs = "";

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

В конце концов в index.php у нас будет примерно следующее:
include "core/lib/module/mod.php"; // главный класс каждого модуля
include "core/engine.php"; // класс псевдо-модуля загрузчика
Engine::loadLibs(); // загрузка всех необходимых библиотек для загрузчика (рекурсивно для удобства)
session_start(); // сессия
$engine = new Engine(); // обьект загрузчика
$engine->enableModules(); // создание обьектов модулей
$engine->modTemplates(); // этапы выполнения методов модулей
ModError::printErrors(); // вывод ошибок (для удобства у нас есть отдельный класс, подключился с библиотеками)


Что же мы имеем в итоге?


Практически полная автоматизация загрузки модулей, полная модульность системы. Работа с БД — модуль mysql. Работа с шаблонами — модуль template. Строка навигации — модуль speedbar. Не нужно? Удалите папку.
С помощью удобных контейнеров шаблонов, вроде <![mod=speedbar]>Speedbar here: <!speedbar> <![/mod=speedbar]>, есть модуль — он работает и секция показывается. Нет модуля — секция вообще не показывается.

На таком принципе можно построить сайт любой сложности используя уже существующие модули плюс написанные специально. Любой модуль может использовать любой иной, а прямое выполнение кода дает чуть ли не 100% гибкость. Нужен ajax? Создаем модуль, который подождет, пока все будет выполнено и подготовлено, а потом в последний момент отменит показ шаблона и покажет только то, что нужно.

Конечно же в такого способа есть и свои недостатки: из за постояной постройки очередей понижается быстродействие, модули могут лезть совсем не туда, куда нужно, и рядовой пользователь, который столкнется со всем этим, попробует поключить пару модулей, верно бросит все это дело.
Но с начала работы я совсем не располагаю на широкую аудиторию, и все изобратенное используется только для себя. И пока «оно» меня вполне устраивает.

Действующую модель этого способа можно посмотреть на моей страничке (ссылка есть в профиле), а исходные коды — в репрозитории GitHub'а.

Спасибо за внимание, если тема заинтересует кого-то, то напишу топик о переходе от теории к практике.
Tags:
Hubs:
Total votes 42: ↑20 and ↓22-2
Comments40

Articles