Apache Zeta Components - high quality PHP components

eZ Components - SignalSlot

Introduction

The SignalSlot component implements a mechanism for inter- and intra-object communication through the use of signals and slots. Signals are emitted through instances of the ezcSignalCollection class and connected to as many functions as needed. A function that is connected to a signal is called a slot. When the signal is emitted, all connected slots will be run in the order they are connected or in the order of the priorities specified when connecting to the signals.

The SignalSlot library supports signals with arguments that are passed on to the slot. Slots must accept all the parameters that are sent to it. The SignalSlot component also supports prioritized slots. Using this mechanism, you can ensure that a slot is run either before or after other slots.

There are many different variations of the Signal/Slot model. The original implementations required you to derive from the Signal class in order to have signals or slots. This is acceptable for languages that support multiple inheritance. Newer implemtentations circumvent this problem by representing signals through one instance of the Signal class. In other words, a class that has two signals will have two member variables holding instances of the Signals class. This approach can lead to the creation of a lot of signal objects, which can be especially problematic for scripting languages. As a result, our signal class can represent several signals.

Class overview

This section gives you an overview of the main classes in the SignalSlot component.

ezcSignalCollection
This class represents a collection of signals. You can emit or connect to the signals using the methods available in this class.
ezcSignalStaticConnections
This class holds the static connections for your application. Static connections are connections to signals that can be emitted by several different instances of ezcSignalCollection.

Usage

A simple signal

This first example shows the simplest possible usage of the signal system. We simply create a new ezcSignalCollection class, connect the "sayHello" signal to the function "hello" and emit the signal.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. function hello()
  4. {
  5.     echo "Hello world\n";
  6. }
  7. $signals = new ezcSignalCollection();
  8. $signals->connect"sayHello""hello" );
  9. $signals->emit"sayHello" );
  10. ?>

The output of this example yields the classic "Hello world".

Slot types

Slots are not limited to normal functions. They are specified using the pseudo type callback. You can specify normal functions, class methods and static class methods, as shown by these variations of the above example.

  1. functions

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. function hello()
  4. {
  5.     echo "Hello world\n";
  6. }
  7. $signals = new ezcSignalCollection();
  8. $signals->connect"sayHello""hello" );
  9. $signals->emit"sayHello" );
  10. ?>
  1. class methods

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class HelloClass
  4. {
  5.     public function hello()
  6.     {
  7.         echo "Hello world\n";
  8.     }
  9. }
  10. $signals = new ezcSignalCollection();
  11. $signals->connect"sayHello", array( new HelloClass(), "hello" ) );
  12. $signals->emit"sayHello" );
  13. ?>
  1. static class methods

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class HelloClass
  4. {
  5.     public static function hello()
  6.     {
  7.         echo "Hello world\n";
  8.     }
  9. }
  10. $signals = new ezcSignalCollection();
  11. $signals->connect"sayHello", array( "HelloClass""hello" ) );
  12. $signals->emit"sayHello" );
  13. ?>

Signals in a class

The typical usage of signals is to have classes emit them when certain events occur. The following example shows a data object that emits a signal when its data is changed. The signal is connected to the caching system, which clears the cache when the data is changed.

This example also shows how you can delay the construction of the ezcSignalCollection object. This can be useful in applications where signals are rarely used and you want to avoid the overhead of creating the object.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class Data
  4. {
  5.     private $signals null;
  6.     public function signals()
  7.     {
  8.         if ( $this->signals == null $this->signals = new ezcSignalCollection();
  9.         return $this->signals;
  10.     }
  11.     public function manipulate()
  12.     {
  13.         // change the data here
  14.         $this->signals()->emit"dataChanged" );
  15.     }
  16. }
  17. class Cache
  18. {
  19.     public function deleteCache()
  20.     {
  21.         echo "Deleting cache\n";
  22.     }
  23. }
  24. $cache = new Cache();
  25. $data = new Data();
  26. $data->signals()->connect"dataChanged", array( $cache"deleteCache" ) );
  27. $data->manipulate();
  28. ?>

Signals with parameters

Signals can have parameters that will be passed on to the receiving slots. The next example is an extension of the previous example, where the data object gives some information about the data that was changed. This information is passed to the receiving slots as a parameter.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class Data
  4. {
  5.     private $signals null;
  6.     public function signals()
  7.     {
  8.         if ( $this->signals == null $this->signals = new ezcSignalCollection();
  9.         return $this->signals;
  10.     }
  11.     public function manipulate()
  12.     {
  13.         // change the data here
  14.         $this->signals()->emit"dataChanged""calendar" );
  15.     }
  16. }
  17. class Cache
  18. {
  19.     public function deleteCache$type )
  20.     {
  21.         echo "Deleting cache for ID: {$type}\n";
  22.     }
  23. }
  24. $cache = new Cache();
  25. $data = new Data();
  26. $data->signals()->connect"dataChanged", array( $cache"deleteCache" ) );
  27. $data->manipulate();
  28. ?>

Note that slots must accept the parameters that are passed to them. You will get errors / warnings if you provide too few parameters to the slots you have connected to a signal.

Also note that it is not possible to pass signal parameters by reference except for object types, which are always references in PHP 5.

Multiple slots

It is possible to connect several slots to one signal. This is just a matter of calling "connect" several times. You can freely mix the different slot types. The following example adds another connection to our previous example. It regenerates the cache after it has been deleted.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class Data
  4. {
  5.     private $signals null;
  6.     public function signals()
  7.     {
  8.         if ( $this->signals == null $this->signals = new ezcSignalCollection();
  9.         return $this->signals;
  10.     }
  11.     public function manipulate()
  12.     {
  13.         // change the data here
  14.         $this->signals()->emit"dataChanged""calendar" );
  15.     }
  16. }
  17. class Cache
  18. {
  19.     public function deleteCache$type )
  20.     {
  21.         echo "Deleting cache for ID: {$type}\n";
  22.     }
  23. }
  24. class CacheGenerator
  25. {
  26.     public function generateCache$identifier )
  27.     {
  28.         echo "Generating cache for ID: {$identifier}\n";
  29.     }
  30. }
  31. $cache = new Cache();
  32. $cacheGenerator = new CacheGenerator();
  33. $data = new Data();
  34. $data->signals()->connect"dataChanged", array( $cache"deleteCache" ) );
  35. $data->signals()->connect"dataChanged", array( $cacheGenerator"generateCache" ) );
  36. $data->manipulate();
  37. ?>

Prioritized slots

The previous example showed how to connect several slots to one signal and how they are executed one after another. The example code is also dependent on the order the slots are executed. Normally, slots are executed in the order they are connected. Sometimes, it is not possible to connect slots in the order you want them to be executed. To ensure that some slots are executed before others, you can use the priority system. When connecting, you can specify a priority for that connection. Priority numbers can be specified using any positive integer. The lower the number, the higher the priority. Higher priority connections are executed first. By default, connections are made with a priority of 1000.

This example shows how the connections from the previous example could have been specified to ensure the order in which the slots are called.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class Data
  4. {
  5.     private $signals null;
  6.     public function signals()
  7.     {
  8.         if ( $this->signals == null $this->signals = new ezcSignalCollection();
  9.         return $this->signals;
  10.     }
  11.     public function manipulate()
  12.     {
  13.         // change the data here
  14.         $this->signals()->emit"dataChanged""calendar" );
  15.     }
  16. }
  17. class Cache
  18. {
  19.     public function deleteCache$type )
  20.     {
  21.         echo "Deleting cache for ID: {$type}\n";
  22.     }
  23. }
  24. class CacheGenerator
  25. {
  26.     public function generateCache$identifier )
  27.     {
  28.         echo "Generating cache for ID: {$identifier}\n";
  29.     }
  30. }
  31. $cache = new Cache();
  32. $cacheGenerator = new CacheGenerator();
  33. $data = new Data();
  34. $data->signals()->connect"dataChanged", array( $cacheGenerator"generateCache" ), 20 );
  35. $data->signals()->connect"dataChanged", array( $cache"deleteCache" ), 10 );
  36. $data->manipulate();
  37. ?>

Excessive usage of the priority system can lead to unmaintainable code, since it is hard to track the various priorities that are in use in a system. We do not recommend using it unless absolutely neccessary.

Static connections

Sometimes it is useful to connect a signal with a specific name, regardless of the sender ezcSignalCollection, to a slot. Consider what would happen if the Data class from our previous example was extended to a DataObject class with a potential of thousands of instances. If you wanted the caching system to work, you would have to connect each one to the caching system upon creation.

This is both unpractical and time consuming. The solution is to use static connections. When creating ezcSignalCollection objects, you can provide an identifier to the constructor. Using ezcSignalStaticConnections, you can connect to all signals from a source with a specific identifier.

The following example shows how to use static connections to connect the signal "dataChanged" from all objects of the type "DataObject" to the caching system.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class DataObject
  4. {
  5.     private $signals null;
  6.     private $identifier null;
  7.     public function __construct$id )
  8.     {
  9.         $this->identifier $id;
  10.     }
  11.     public function signals()
  12.     {
  13.         if ( $this->signals == null $this->signals = new ezcSignalCollection__CLASS__ );
  14.         return $this->signals;
  15.     }
  16.     public function manipulate()
  17.     {
  18.         // change the data here
  19.         $this->signals()->emit"dataChanged"$this->identifier );
  20.     }
  21. }
  22. class Cache
  23. {
  24.     public function deleteCache$identifier )
  25.     {
  26.         echo "Deleting cache for ID: {$identifier}\n";
  27.     }
  28. }
  29. $cache = new Cache();
  30. ezcSignalStaticConnections::getInstance()->connect"DataObject",
  31.                                                     "dataChanged", array( $cache"deleteCache" ) );
  32. $data1 = new DataObject);
  33. $data2 = new DataObject);
  34. $data1->manipulate();
  35. $data2->manipulate();
  36. ?>

You can freely mix static connectons and normal connections. Static connections with the same priority as normal connections are executed after the normal connections.

Lazy initialization

Lazy initialization is a mechanism to load and configure a component, only when it is really used in your application. This mechanism saves time for parsing the classes and configuration, when the component is not used at all during one request. You can find a description how you can use it for your own components and how it works in the ezcBase tutorial. The keyword for the signal slot component is ezcInitSignalStaticConnections.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. class customLazySignalConfiguration implements ezcBaseConfigurationInitializer
  4. {
  5.     public static function configureObject$signal )
  6.     {
  7.         $signal->connect'default''signal''customFunction' );
  8.     }
  9. }
  10. ezcBaseInit::setCallback
  11.     'ezcInitSignalStaticConnections'
  12.     'customLazySignalConfiguration'
  13. );
  14. function customFunction()
  15. {
  16.     echo "Custom function called with:\n"var_exportfunc_get_args(), true );
  17. }
  18. $signals = new ezcSignalCollection();
  19. $signals->emit'signal'42 );
  20. ?>

This example shows a very simple signal setup with only one signal mapped to a custom function, which just dumps the passed data. The main difference, compared to earlier examples, is that we roll out the configuration to an own class, and define a callback using ezcBaseInit::setCallback to this class, which will be called with the static connections manager as a parameter on the first request for a yet uninitialized signal connection.

ezcBaseInit::setCallback accepts as a first parameter a component specific key, which lets the component later request the right configuration callback. This class must implement the ezcBaseConfigurationInitializer class. The second parameter is the name of the class to perform the static callback on. Each component's lazy initialization calls the static method configureObject() on the referenced class.

When a signal connection is really created and used, like shown in line 22 of the example, the collection will be configured by the custom configuration class. The used identifier "default" may be used for the default signal collection. It of course works for other signal collections, too, like the example for static connections shows.