Сигналы и слоты в PHP. Такие же как в Qt. Ну почти
Сигналы и слоты — подход, используемый в некоторых языках программирования и библиотеках (например, Boost и Qt) который позволяет реализовать шаблон «наблюдатель», минимизируя написание повторяющегося кода. Концепция заключается в том, что компонент (часто виджет) может посылать сигналы, содержащие информацию о событии (например: был выделен текст «слово», была открыта вторая вкладка). В свою очередь другие компоненты могут принимать эти сигналы посредством специальных функций — слотов. Система сигналов и слотов хорошо подходит для описания Графического интерфейса пользователя. Также механизм сигналов/слотов может быть применён для асинхронного ввода-вывода (включая сокеты, pipe, устройства с последовательным интерфейсом, др.) или уведомления о событиях. В библиотеке Qt благодаря Метаобъектному компилятору (англ.)русск. отпадает необходимость писать код регистрации/дерегистрации/вызова, так как эти шаблонные участки кода генерируются автоматически.Говорит нам Википедия.
В php приложениях нет никакого event loop. Получили запрос, отдали ответ. И все тут. Однако между «получили запрос» и «отдали ответ» есть какой-то жизненный цикл приложения, соответственно не все потеряно и можно попробовать применить этот механизм в веб приложениях. В студенчестве я программировал на C++/Qt и его реализация сигналов и слотов очень тогда нравилась. Отличная ведь идея. Ближайший родственник — шаблон «Наблюдатель». Цель одна, однако реализации совершенно разные. Пришла мне в голову мысль реализовать данный механизм для php в виде небольшой библиотеки.
Сигналы и слоты
Сигнал — это нечто, что может сообщить внешнему Миру о внутреннем состоянии объекта. Телефон может звонить, кот может «мяукать». Звонки и «мяу» являются сигналами телефона и кота соответственно. Они сообщают нам об изменении внутреннего состояния объектов: телефон из состояния покоя перешел в состояние «Вам звонят», кот из состояния «сытый кот» перешел в состояние «голодный кот».
Вы можете реагировать на эти сигналы каким либо способом. Например: снять трубку или накормить наконец-таки своего кота. Ваши реакции на сигналы — это слоты.
В программировании возникают точно такие же ситуации, когда нам нужно реагировать на изменение состояния какого-либо объекта. Для этого и используется механизм сигналов и слотов — для обеспечения коммуникации между объектами. На мой взгляд, в отличии от шаблона «Наблюдатель» он проще и намного прозрачнее в использовании (хотя «Наблюдатель» сам по себе тоже очень простой шаблон).
Немного поразмыслив, я написал библиотеку connector для php, реализующую механизм сигналов и слотов. Посмотрим на нее?
Установка
Connector доступен как composer пакет, соответственно для установки необходимо выполнить
composer require fluffy/connector
или добавить зависимость на библиотеку в Ваш composer.json файл
"require": {
...
"fluffy/connector": "^1.0"
}
и выполнить
composer update
Использование
1. Сигналы
Если мы хотим, чтобы объект мог испускать сигналы, то нам нужно унаследовать свой класс от
Signal
класса. Например, у нас есть логер класс и мы хотим, чтобы он испускал сигнал somethingIsLogged
всякий раз, когда он заканчивает логирование: emit('somethingIsLogged', 'Some useful data');
}
}
Для того, чтобы выслать сигнал, необходимо вызвать метод
emit
и передать два параметра: имя сигнала и данные, которые будут переданы этим сигналом. Можно передавать любой тип данных: строку, число, массив или объект. Это все. Теперь объект логер умеет высылать сигналы во внешний мир. Но пока никто не подключен к этому сигналу, никто не будет знать о том, что этот объект сообщает о чем-то полезном. Давайте это исправим.2. СлотыСлот — это обычный метод класса, который начинается с ключевого слова
slot
. Давайте создадим класс со слотом.
3. Сигнально слотовые соединенияИтак, у нас есть
Logger
класс с сигналом и Receiver
класс со слотом. Для того, чтобы реагировать на сигнал, необходимо подключить к нему слот.use Fluffy\Connector\ConnectionManager;
$logger = new Logger();
$receiver = new Receiver();
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
$logger->log();
Вызвав
ConnectionManager::connect($sender, $signalName, $receiver, $slotName)
мы тем самым подключили сигнал объекта логера к слоту объекта получателя. Это значит, что всякий раз при вызове $logger->log()
будет высылаться сигнал логера somethingIsLogged
, на который будет реагировать слот slotReactOnSignal
объекта получателя. Мы можем определить столько сигнально слотовых соединений, сколько нам нужно. Работают все возможные виды соединений: - Один сигнал к одному слоту
- Один сигнал к нескольким слотам
- Несколько сигналов к нескольким слотам
- Несколько сигналов к одному слоту
При чем неважно, будь то сигналы от одного объекта или от разных, будь то слоты одного объекта или разных.4. Типы соединений
По умолчанию метод
ConnectionManager::connect()
создает перманентное соединение. Это значит, что соединение не будет разорвано после первой высылки сигнала. Однако есть возможность создать одноразовое соединение, передав пятым параметром константу — тип соединения. Например: use Fluffy\Connector\ConnectionManager;
$logger = new Logger();
$receiver = new Receiver();
ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal', ConnectionManager::CONNECTION_ONE_TIME);
$logger->log();
// Log once again.
$logger->log();
После второго вызова
Logger::log()
ничего не произойдет так как слот будет отключен от сигнала после первой его высылки.Для чего это нужно? - Мне просто нравится механизм сигналов и слотов и его реализация в Qt. Возникло желание реализовать нечто подобное для php.
- Это удобнее чем «Наблюдатель».
Репозиторий проекта: fluffy/connector
Надеюсь, было интересно дойти до этой строки. Критика и мысли в слух приветствуются.
Комментарии (16)
7 июля 2016 в 03:16
0↑
↓
Мне кажется Rx библиотека более гибкая в использовании чем сигналы и слоты. Как реализовывать на сигналах ситуацию: doSomething () только после того, как получены два сигнала? Придется пилить свой велосипед, который опять таки станет Rx7 июля 2016 в 03:20 (комментарий был изменён)
0↑
↓
Никак и не реализовать такое поведение на сигналах. Хм. Можете привести реальный пример, когда такое поведение необходимо?7 июля 2016 в 03:44
0↑
↓
Например после двух запросов к бд (за новостями и комментариями к ним). по отдельности они нас не интересуют, а вот если вместе — то записать в лог/внести подозрительный ip в бд7 июля 2016 в 03:55 (комментарий был изменён)
+1↑
↓
И релизовать можно. Через посредника, который подпишется на оба сигнала, и при их получении выдаст третий, на который уже подпишется целевой слот., но опять таки, уж лучше Rx делать. Тем более что он поддерживает асинхронную работу.7 июля 2016 в 03:57
0↑
↓
Верно. Поспешил я с ответом.
7 июля 2016 в 03:52 (комментарий был изменён)
+2↑
↓
Мне видится несколько неудобным abstract class Signal. Я бы еще понял, если MeowSignal extends Signal, а с предлагаемым способом использования это выглядит странно: логгер в примере вынужденно «является» сингалом (is-a relation), хотя по сути то это не так; да и что делать, если у меня уже есть иерархия наследования? Может, в Qt оно так и сделано (хотя не знаю, не видел), но в С++ есть множественное наследование ведь.Думаю, было бы уместнее либо трейт, либо Signal: emit () (либо оба варианта).
7 июля 2016 в 03:56
+1↑
↓
Действительно. Если иерархия уже имеется, то абстрактный класс не очень удобен. С трейтом идея нравится.7 июля 2016 в 07:20 (комментарий был изменён)
0↑
↓
Тоже сразу при прочтении статьи обратил внимание, что трейт был бы уместнее.7 июля 2016 в 07:26
0↑
↓
Банально в случае, если отнаследовался от сторонней библиотеки.
7 июля 2016 в 04:00 (комментарий был изменён)
0↑
↓
по сути Signal класс должен называться Signalable, тогда проблема с восприятием отойдет. Но так только интерфейсы принято именовать. Поэтому оставил Signal. Наверное, самый верный вариант в моем случае — это трейт.И в Qt сделано не так, не надо плохо о нем думать)
7 июля 2016 в 08:59
0↑
↓
Зато Signalable — отличное имя для трейта, как писали выше;)
7 июля 2016 в 06:58
0↑
↓
ИМХО, механизм сигналов и слотов сам по себе хорош, но что касаемо данного примера… я не могу сказать, что меня это вдохновило, извините. СигнатураConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
Меня немного смущает… Довольно простой пример, и в нем уже такая простыня…, а если задачи усложнятся по тем вариантам, которые вы привели?- Один сигнал к нескольким слотам
- Несколько сигналов к нескольким слотам
- Несколько сигналов к одному слоту
И как мне установить в одном соединении несколько ресиверов? К примеру я создал 3 класса Receiver1. Receiver2, Receiver3 и заимлиментился от Receiveable который реализовал slotReactOnSignal (). Далее, я хочу сделать так:ConnectionManager::connect($logger, 'somethingIsLogged', [$receiver1, $receiver2, $receiver3], 'slotReactOnSignal');
А если добавить в эту логику внутреннее общение между слотами…
Как мне видится, я полностью согласен с webmasterx все придет к тому, что нужен Rx.
Не хочу сказать, что она бесполезная, я просто не вижу область применений) А не можете дополнить статью еще какими-либо примерами?)) Чтобы ощутить полезность вашей библиотеки) Хотелось бы ее использовать, но где ее юзануть так, чтобы она была прям полезной…7 июля 2016 в 07:28
0↑
↓
Эта сигнатура пришла исключительно из Qt и мне тоже не нравится. Но зато она знакома Qt кодерам. А для нескольких ресиверов нужно просто повторитьConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
И пременения я тоже не нашел. В Yii2 можно подключиться к событиям модели, думаю и к другим классам тоже, и в других фреймворках тоже найдется аналог.
Но в итоге если нужна событийная реализация — приходим к тому же Rx.ИМХО: сигналы и слоты в Qt либо пережиток прошлого (не догадались до абстракции Rx), либо умышленное упрощение в сторону скорости.
7 июля 2016 в 08:31
0↑
↓
По поводу сигнатуры. Что в ней такого страшного? Вроде бы все логично и интуитивно понятно: сигнал от отправителя соединяется со слотом получателя.По поводу соединений — посмотрите тесты. Как указал webmasterx будет несколько последовательных вызовов ConnectionManager: connect ().
По поводу применения. Вначале статьи я писал, что сигналы и слоты отлично подходят для gui приложений с event loop. Там они смотрятся на своем месте. Что же касается реального применения в скриптовом php: сигалы и слоты можно использовать там, где подходит наблюдатель. Например выполнить действие перед сохранением сущности. Или же (о боже мой максимализм) переписать старый процедурный подход в приложении с хуков на сигналы.
Вообще, конечно же, just for fun. Узнал как пишутся composer пакеты.
7 июля 2016 в 09:21
0↑
↓
, а чем вам ивенты не угодили?7 июля 2016 в 09:38
0↑
↓
Есть отличная библиотека evenement/evenement с похожим функционалом и на трейтах.