Pull to refresh

Архитектура SignalSlot для РНР веб-приложений на примере ezComponents

Reading time4 min
Views660
logo1На днях, читая рассылку по Zend Frameworks я заметил тему одного разработчика о реализации системы плагинов без модификации некоторого стандартного ядра. Подобную задачу приходится решать достаточно часто и во многих случаях — например, вряд ли хоть какая-то CMS-система обходится без механизма плагинов. Конечно же, разработчики таких популярных CMS систем, как Drupal или Wordpress уже решили для себя эту задачу, разработав собственную архитектуру подключения плагинов на лету без затрагивания функционала ядра. Однако аналогичная задача, мне кажется, все же из категории «вечных» и не все решения могут быть применены в каждом конкретном случае.

С аналогичными проблемами сталкиваются не только веб-разработчики, она актуальна и при проектировании компонентных десктопных приложений и сложных систем. И некоторые успешные решения вполне можно подсмотреть и позаимствовать с таких разработок. В данном случае я говорю об архитектуре Signal/Slot, которая реализована в библиотеке Qt (подробное описание) и применяется там для коммуникации между компонентами. Аналогичный функционал очень был бы полезен и в веб-разработках, в данном случае — в РНР проектах.

Мне лично не хватает событий в РНР, аналогично JavaScript, а Signal/Slot как раз позволит получить аналогичный функционал и на серверной стороне. Если кратко, то каждый объект может генерировать некоторое событие (сигнал), а другие объекты подписываются на нужные сигналы, и при наступлении сигнала вызываются все зарегистрированные функции (слоты), слушающие указанный сигнал. Для такой архитектуры нужен промежуточный объект, который будет хранить список всех зарегистрированных сигналов, а также вести реестр слушающих функций, а при наступлении события — запускать их в нужной очередности. А есть ли готовое решение такого функционала для РНР проектов, или необходимо писать собственное? Конечно, такой скрипт написать достаточно просто, у опытного разработчика на это уйдет от силы день-два, но можно использовать и готовое решение, входящее в состав фреймворка ezComponents, к которому я давно уже испытываю некоторую слабость, несмотря на то, что есть и более продвинутые и серьезные решения, вроде Zend Framework (жаль, в нем все же нет такого модуля).

В фреймворке есть компонент ezcSignalCollection, который как раз и реализует основной компонент архитектуры. После создания экземпляра объекта вы можете подключать любое количество сигналов и слотов, просто вызывая метод connect. Например, для того, чтобы наша функция _test_slot() которая просто выводит строку про время вызова (через оператор echo), реагировала на сигнал «test_alert» (а сигналом может быть любая строка, но лучше всего создать себе некоторый репозитарий заранее определенных сигналов), нужно просто подключить ее:

$tmp = new ezcSignalCollection();

function _test_slot()
{
echo 'Called by test_alert signal!';
}

$tmp->connect("test_alert", "_test_slot");

//а теперь генерирует тестовый сигнал

$tmp->emit("test_alert");

//мы должны получить в результате исполнения строку:
Called by test_alert signal!


После подключения мы в любом месте можем вызвать метод emit, который генерирует указанный сигнал. Заметьте, что при подключении вы должны передать имя функции в виде строки (так как применяется PHP функция call_user_func) Это самый просто пример. Вы можете на одно событие/сигнал повесить несколько функций и они будут исполнены в той последовательности, как они были подключены. Однако, такое поведение не всегда полезно. Для этого есть механизм приоритетов — каждому слоту можно сопоставить уровень приоритета (целое число, 1 — 65 536), и при вызове он будет учитываться — чем меньший номер, тем выше будет этот слот, то есть слот с приоритетом 1 будет выполнен первым, а потом далее, вплоть до последнего. Если у нескольких слотов одинаковый приоритет (а по умолчанию он 1000), то они будут исполнятся в порядке подключения. Установить приоритет просто — передайте третьим аргументом в меток connect необходимый приоритет.

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

Если вам необходимо вместе с сигналом передать некоторые параметры, то их нужно добавить после самого сигнала при генерации в методе еmit. Таким образом можно передать неограниченное количество параметров, которые будут переданы каждой функции. Однако учитывайте нюансы, в части передачи значений по ссылке — детальнее следует обратится с PHP Manual в раздел call_user_func_array.

Однако в случае больших и сложных приложений одного этого функционала может быть недостаточно. И не столько из-за ограничений, сколько из-за трудоемкости — сигналы должны быть уникальными, но стремление сделать унифицированную систему приведет нас к тому, что в разных модулях могут быть одинаковые сигналы, то есть желательно их сделать одинаковыми для облегчения понимания и читабельности, но вот нельзя. Но есть выход — мы можем создать статическое подключение и определить метод или функцию, которая будет реагировать на сигналы, сгенерированные объектами одного класса. То есть, сигнал «delete_item» сгенерированный классом Cache будет обработан свои обработчиком, а такой же сигнал, но от наследника класса News будет обработан уже своим образом. При этом можно совмещать как обычную подписку на сигналы, так и статическую, учитывая, что статично подключенные функции будут отработаны после обычных. Например, метод _prepare_item будет вызван в обоих случаях, а после его отработки будет уже вызываться необходимый статически подключенный обработчик — так можно реализовать, по сути, препроцессор обработки.

Для реализации статичных слотов необходимо при создании объекта ezcSignalCollection передать конструктору имя класса, при этом весь остальной код не изменяется, а для подключения использовать класс ezcSignalStaticConnections.

Сам код этого модуля достаточно прост, если не сказать тривиален, и поискав в Google (например, это и это), я на нескольких форумах нашел другие варианты собственной реализации, однако, этот компонент из ezComponents все же более гибкий и функциональный. Думаю, вам пригодится такой механизм, если вы планируете построить гибкую систему с плагинами. Хотя даже в случае монолитного приложения реализация Signal/Slot может помочь в реализации более гибкой и красивой архитектуры. Просто попробуйте.
Tags:
Hubs:
+21
Comments15

Articles