Сигналы и слоты — подход, используемый в некоторых языках программирования и библиотеках (например, 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. Сигналы

Если мы хотим, чтобы объект мог испускать сигналы, то нам нужно имплементировать интерфейс SignalInterface и использовать соответствующий трейт SignalTrait. Например, у нас есть логер класс и мы хотим, чтобы он высылал сигнал somethingIsLogged всякий раз, когда он заканчивает логирование:
<?php

/**
 * @file
 * Contains definition of Logger class.
 */

use Fluffy\Connector\Signal\SignalInterface;
use Fluffy\Connector\Signal\SignalTrait;

/**
 * Class Logger.
 */
class Logger implements SignalInterface {
    use SignalTrait;

    public function log() 
    {
        // Do logging stuff.
        ...

        // Emit signal about successfull logging.
        $this->emit('somethingIsLogged', 'Some useful data');
    }
}

Для того, чтобы выслать сигнал, необходимо вызвать метод emit и передать два параметра: имя сигнала и данные, которые будут переданы этим сигналом. Можно передавать любой тип данных: строку, число, массив или объект. Это все. Теперь объект логер умеет высылать сигналы во внешний мир. Но пока никто не подключен к этому сигналу, никто не будет знать о том, что этот объект сообщает о чем-то полезном. Давайте это исправим.

2. Слоты

Слот — это обычный метод класса. Давайте создадим класс со слотом.
<?php

/**
 * @file
 * Contains definition of Receiver class.
 */

/**
 * Class Receiver.
 */
class Receiver
{

  public function slotReactOnSignal($dataFromSignal) {
    echo "Received data: $dataFromSignal";
  }

}


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() ничего не произойдет так как слот будет отключен от сигнала после первой его высылки.

5. Disconnect

Если мы не хотим больше слушать определенный сигнал, то просто отключаемся от него
ConnectionManager::disconnect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');

Если нам необходимо сбросить все существующие соединения, то нужно вызвать
ConnectionManager::resetAllConnections()


Для чего это нужно?

  • Мне просто нравится механизм сигналов и слотов и его реализация в Qt. Возникло желание реализовать нечто подобное для php.
  • Это удобнее чем «Наблюдатель».


Репозиторий проекта: fluffy/connector

Надеюсь, было интересно дойти до этой строки. Критика и мысли в слух приветствуются.

UPD


  • Как заметил symbix использование наследования для реализации функционала сигнала не удобно. Учтено. Теперь реализовано на трейте (см. пункт 1)
  • Как заметил oxidmod не имеет особого смысла ограничивать именование слотов (ключевое слово «slot»). Теперь слотом может быть метод с любым именем (см. пункт 2)
Поделиться с друзьями
-->

Комментарии (35)


  1. webmasterx
    07.07.2016 03:16

    Мне кажется Rx библиотека более гибкая в использовании чем сигналы и слоты. Как реализовывать на сигналах ситуацию: doSomething() только после того, как получены два сигнала? Придется пилить свой велосипед, который опять таки станет Rx


    1. FluffyMan
      07.07.2016 03:20

      Никак и не реализовать такое поведение на сигналах. Хм. Можете привести реальный пример, когда такое поведение необходимо?


      1. webmasterx
        07.07.2016 03:44

        Например после двух запросов к бд (за новостями и комментариями к ним). по отдельности они нас не интересуют, а вот если вместе — то записать в лог/внести подозрительный ip в бд


      1. webmasterx
        07.07.2016 03:55
        +1

        И релизовать можно. Через посредника, который подпишется на оба сигнала, и при их получении выдаст третий, на который уже подпишется целевой слот. но опять таки, уж лучше Rx делать. Тем более что он поддерживает асинхронную работу.


        1. FluffyMan
          07.07.2016 03:57

          Верно. Поспешил я с ответом.


  1. symbix
    07.07.2016 03:52
    +3

    Мне видится несколько неудобным abstract class Signal. Я бы еще понял, если MeowSignal extends Signal, а с предлагаемым способом использования это выглядит странно: логгер в примере вынужденно «является» сингалом (is-a relation), хотя по сути то это не так; да и что делать, если у меня уже есть иерархия наследования? Может, в Qt оно так и сделано (хотя не знаю, не видел), но в С++ есть множественное наследование ведь.

    Думаю, было бы уместнее либо трейт, либо Signal::emit() (либо оба варианта).


    1. FluffyMan
      07.07.2016 03:56
      +1

      Действительно. Если иерархия уже имеется, то абстрактный класс не очень удобен. С трейтом идея нравится.


      1. Blumfontein
        07.07.2016 07:20

        Тоже сразу при прочтении статьи обратил внимание, что трейт был бы уместнее.


        1. Blumfontein
          07.07.2016 07:26

          Банально в случае, если отнаследовался от сторонней библиотеки.


    1. FluffyMan
      07.07.2016 04:00

      по сути Signal класс должен называться Signalable, тогда проблема с восприятием отойдет. Но так только интерфейсы принято именовать. Поэтому оставил Signal. Наверное, самый верный вариант в моем случае — это трейт.

      И в Qt сделано не так, не надо плохо о нем думать)


      1. andrewnester
        07.07.2016 08:59

        Зато Signalable — отличное имя для трейта, как писали выше ;)


    1. FluffyMan
      07.07.2016 11:09
      +1

      Переписал на трейте. Обновлен пункт 1


      1. symbix
        08.07.2016 04:30
        +2

        В composer.json еще забыли версию php поменять. Трейты доступны начиная с 5.4.


        1. FluffyMan
          08.07.2016 11:21

          Точно. Спасибо!


  1. Scrobot
    07.07.2016 06:58
    +1

    ИМХО, механизм сигналов и слотов сам по себе хорош, но что касаемо данного примера… я не могу сказать, что меня это вдохновило, извините. Сигнатура

    ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
    

    Меня немного смущает… Довольно простой пример, и в нем уже такая простыня… а если задачи усложнятся по тем вариантам, которые вы привели?
    • Один сигнал к нескольким слотам
    • Несколько сигналов к нескольким слотам
    • Несколько сигналов к одному слоту

    И как мне установить в одном соединении несколько ресиверов? К примеру я создал 3 класса Receiver1. Receiver2, Receiver3 и заимлиментился от Receiveable который реализовал slotReactOnSignal(). Далее, я хочу сделать так:
    ConnectionManager::connect($logger, 'somethingIsLogged', [$receiver1, $receiver2, $receiver3], 'slotReactOnSignal');
    

    А если добавить в эту логику внутреннее общение между слотами…
    Как мне видится, я полностью согласен с webmasterx все придет к тому, что нужен Rx.
    Не хочу сказать, что она бесполезная, я просто не вижу область применений) А не можете дополнить статью еще какими-либо примерами? )) Чтобы ощутить полезность вашей библиотеки) Хотелось бы ее использовать, но где ее юзануть так, чтобы она была прям полезной…


    1. webmasterx
      07.07.2016 07:28

      Эта сигнатура пришла исключительно из Qt и мне тоже не нравится. Но зато она знакома Qt кодерам. А для нескольких ресиверов нужно просто повторить

      ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');
      

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

      ИМХО: сигналы и слоты в Qt либо пережиток прошлого (не догадались до абстракции Rx), либо умышленное упрощение в сторону скорости.


    1. FluffyMan
      07.07.2016 08:31

      По поводу сигнатуры. Что в ней такого страшного? Вроде бы все логично и интуитивно понятно: сигнал от отправителя соединяется со слотом получателя.

      По поводу соединений — посмотрите тесты. Как указал webmasterx будет несколько последовательных вызовов ConnectionManager::connect().

      По поводу применения. Вначале статьи я писал, что сигналы и слоты отлично подходят для gui приложений с event loop. Там они смотрятся на своем месте. Что же касается реального применения в скриптовом php: сигалы и слоты можно использовать там, где подходит наблюдатель. Например выполнить действие перед сохранением сущности. Или же (о боже мой максимализм) переписать старый процедурный подход в приложении с хуков на сигналы.

      Вообще, конечно же, just for fun. Узнал как пишутся composer пакеты.


  1. oxidmod
    07.07.2016 09:21
    +3

    а чем вам ивенты не угодили?


    1. FluffyMan
      08.07.2016 12:53

      Угодили. Спортивный интерес. Ваш вопрос можно задать и относительно к evenement/evenement


  1. Borro
    07.07.2016 09:38

    Есть отличная библиотека evenement/evenement с похожим функционалом и на трейтах.


    1. oxidmod
      07.07.2016 10:12

      или соотвествующая компонента symfony
      http://symfony.com/doc/current/components/event_dispatcher/introduction.html


      1. FluffyMan
        07.07.2016 11:36

        Это ведь реализация шаблона «Медиатор». Я вот только что осознал, что сигналы и слоты — больше «Медиатор», чем «Наблюдатель». Так как возможны соединения не только один к одному, но и многие ко многим.


    1. FluffyMan
      07.07.2016 11:29

      Спасибо, не видел. Подход почти такой же. С одним различием, что там классический «Наблюдатель», где наблюдаемый объект содержит в себе список всех наблюдателей. В сигналах и слотах маппинг объект.сигнал -> объект.слот не хранится в наблюдаемом объекте.

      Очень похожа) И судя по всему очень популярна. Ну, хотя бы мой спортивный интерес удовлетворен.


      1. oxidmod
        07.07.2016 11:36

        не уверен что правильно вас понял. маппинг объект.сигнал -> объект.слот хранится в диспетчере.
        вы можете иметь столько диспетчеров сколько вам нужно. Допустим каждая подсистема может иметь свой диспетчер который связывает компоненты подсистемы + общий диспетчер всег оприложения, который связывает между собой подсистемы. При ленивой инициализации лишние диспетчеры/листенеры не будут созданы что улучшит общий перфоманс приложения.

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


        1. FluffyMan
          07.07.2016 11:40

          Мой комментарий был по поводу evenement/evenement, которая является классической реализацией шаблона «Наблюдатель». В этой библиотеке все наблюдатели хранятся внутри наблюдаемого объекта, а не в диспетчере. Посмотрите реализацию.

          Про диспетчер Вы, наверное, имели в виду symfony event dispatcher. Там согласен, маппинг объект.сигнал -> объект.слот хранится в диспетчере.


          1. FluffyMan
            07.07.2016 11:45

            evenement/evenement все же не классическая реализация наблюдателя. все же наблюдатели привязываются к событию. но суть та же.


  1. oxidmod
    07.07.2016 11:30

    и еще впрос, почему метод слот должен начинаться с «ключевого» слова slot
    в свете
    ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');

    смысла в этом нет.

    есть смысл это сделать, если название метода slotReactOnSignal генерируется автоматически из somethingIsLogged
    типа slotOnSomethingIsLogged и автоматически вызывается или бросает исключение, если метод не определн

    тогда
    ConnectionManager::connect($logger, 'somethingIsLogged', $receiver, 'slotReactOnSignal');


    можно заменить на
    ConnectionManager::connect($logger, 'somethingIsLogged', $receiver);

    или по крайней мере сделать аргумент опциональным, если по какойто причине нужно использовать не стандартно названный метод


    1. FluffyMan
      07.07.2016 11:33

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


    1. FluffyMan
      08.07.2016 13:01

      Вы, в общем-то, правы. Убрал ограничение на именование слотов.


  1. IvanPanfilov
    07.07.2016 12:35

    штука чисто для фанатов сигналов и слотов — для людей которые писали на Qt и хотят похожего поведения.

    ну а для нормальных людей есть привычные «события» удобно использовать, читабельный код — просто и понятно


    1. FluffyMan
      07.07.2016 12:42

      Ну что Вы так сразу «ну а для нормальных людей». У Вас вон вообще, аватар — шоколадка)


      1. oxidmod
        07.07.2016 13:09

        это местное тро-ло-ло, не обращайте внимания)


  1. 4orever
    07.07.2016 16:00

    Чем, кроме названия, это отличается от EventManager в ZF2 и аналогичного в Symfony?


    1. FluffyMan
      08.07.2016 12:54

      По сути ничем. Еще одна реализация взаимодействия между объектами.


  1. Mistx
    07.07.2016 16:44

    Для PHP механизм signal-slot не требуется, т.к. отсутствует потоковая модель (все функции и методы в PHP блокирующие) и необходимость между этими потоками общаться. А если вы при помощи данного механизма желаете навешивать некоторые стандарные поведения на группы объектов, то лучше использовать одноименный шаблон Behavior (поведение) — значительно красивее и понятнее получится.