Архитектура SignalSlot для РНР веб-приложений на примере ezComponents
Приветствуем наших читателей. Что-то занявшись глобальными задачами и разработкой, открою секрет, платформы для разработки и запуска онлайновых браузерных или клиентских (полу-клиентских, в смысле RIA) игр, я немного упустил из вида более простые и понятные, востребованные темы. На днях, читая рассылку по Zend Framework я заметил тему одного разработчика о реализации системы плагинов без модификации некоторого стандартного ядра.
Подобную задачу приходится решать достаточно часто и во многих случаях - например, вряд ли хоть какая-то 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 <b>test_alert</b> signal!';
- }
- $tmp->connect("test_alert", "_test_slot");
- //а теперь генерирует тестовый сигнал
- $tmp->emit("test_alert");
- //мы должны получить в результате исполнения строку:
- Called by <b>test_alert</b> signal!
* This source code was highlighted with Source Code Highlighter.
После подключения мы в любом месте можем вызвать метод 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 может помочь в реализации более гибкой и красивой архитектуры. Просто попробуйте.
А ведь это действительно пишется за несколько дней — надо будет попробовать.
Исправьте — рассылку по Zend Frameworks
да поправил, спасибо.
Если напишете для зенда модуль — поделитесь, мне бы пригодилось!
Не вопрос — главное найти время
> здесь я пока не понял одного момента — если объект уже создан, будет вызван метод этого объекта, а если нет — создан новый экземпляр или как?
в примерах есть такое, думаю станет ясно
connect( «sayHello», array( new HelloClass(), «hello» ) );
$signals->emit( «sayHello» );
?>
гм… это вроде, если я вручную задаю создание нового класса. Но! в реальном приложении это должно обрабатываться как то, ведь создание объекта в большом приложении может быть сложной операцией, а так ограничение, что конструктор должен создавать объект как минимум не принимая параметров. Но спасибо за подсказку!
Архитектура вызывает неподдельный интерес, но почему то у меня стойкое предубеждение о её малоприменимости в написании веб-приложений. Очень хочется увидеть пост об использовании Signal/Slot в конкретных случаях на PHP.
она может быть применена в абсолютно любом приложении, в статье описаны, насколько я помню, некоторые варианты. Но лучший способ — взять и попробовать самому.
Меня тоже большие сомнения о целесообразности применения такого подхода в web-приложениях. На десткопе оно понятно, объекты созданы единожды и живут на протяжении работы всего приложения, обмен сообщениями между ничи через сигналы и слоты смотрится естественно. В вебе же сплошной bootstrap, это линейный в своей основе процесс, объекты создаются и уничтожаются в рамках одного запроса и зачастую последовательность запуска функций предсказуема. Сигналы и слоты, как мне кажется, тут избыточны, по крайней мере у меня нет никаких идей по поводу того, как можно было бы в вебе применить этот, без сомнения мощный и удобный инструмент. Это просто не типичная для него сфера применения.
> но почему то у меня стойкое предубеждение о её малоприменимости в написании веб-приложений
Насколько понятно из статьи — это фактически простой event dispatcher
На сегодняшний день многие фреймворки и кмс это умеют, просто мало кто доходит до применения его. Т.е. если покопать код инициализации этих движков MVC и EventD совмещены. И не является чем то новым в разработке.
Почему здесь говориться о сроке написания день-два? Я для себя написал такой класс за ~3 часа.
Стоило бы еще заметить, что обработчик События может вернуть результат. А результат должен быть ввиде массива ответов слушателей.
Например, модуль который использует параметр Место на сервере (папка) может при выводе на экран запрашивать событие «get_dir_list-html» (с параметрами — имя которое будут спрашивать в _POST, существующее значение и т.д.) и если есть слушатель на это сообщение, то он вернет html-код контрола, который скриптами дает возможность выбрать папку, иначе (вызов события вернул код отсутствия слушателей) показать свой контрол. При сабмите формы, если не указывалось имя поля или значение не заполнено, вызывать другое событие get_dir_list-data которое запросит у стороннего слушателя нужные данные.
@Антон
Чуток полазил по Вашеу блогу… похоже, мой комментарий немного не в тему.