Сигналы и слоты в 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 () только после того, как получены два сигнала? Придется пилить свой велосипед, который опять таки станет Rx
    • 7 июля 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 с похожим функционалом и на трейтах.

© Habrahabr.ru