
С аналогичными проблемами сталкиваются не только веб-разработчики, она актуальна и при проектировании компонентных десктопных приложений и сложных систем. И некоторые успешные решения вполне можно подсмотреть и позаимствовать с таких разработок. В данном случае я говорю об архитектуре 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 может помочь в реализации более гибкой и красивой архитектуры. Просто попробуйте.