📜 ⬆️ ⬇️

ZF2 EventManager

A slightly free translation of the EventManager in Zend Framework 2 from the Matthew Weier O'Phinney blog .
The article in the examples tells about what Zend\EventManager , how to use it, what advantages are provided by the event method of solving programmer tasks in PHP. About what's waiting for us in ZF2.
The original and the translation was written with the release of zf2.dev4, before .beta1, no significant changes occurred. But still, the article should be used for review, no more.

Terminology



An event is an object containing data, when and how it was triggered: what object caused it, passed parameters, etc. The event also has a name, which allows you to bind handlers to a specific event, referring to the name of this event.

Let's start


The minimum things you need to work with all this:

 use Zend\EventManager\EventManager; $events = new EventManager(); $events->attach('do', function($e) { $event = $e->getName(); $params = $e->getParams(); printf( 'Handled event "%s", with parameters %s', $event, json_encode($params) ); }); $params = array('foo' => 'bar', 'baz' => 'bat'); $events->trigger('do', null, $params); 


At the output we get:
  Handled event "do", with parameters {"foo": "bar", "baz": "bat"} 

Nothing complicated!
Note: The examples use an anonymous function, but you can use the name of a function, a static class method, or an object method.

But what about the second “null” argument in the $events->trigger() method?
')
Typically, an EventManager object EventManager used within a class, and an event is triggered within a method of this class. And this second argument is the "context", or the "target", and in the described case, would be an instance of this class. This provides access for event handlers to the request object, which can sometimes be useful / necessary.

 use Zend\EventManager\EventCollection, Zend\EventManager\EventManager; class Example { protected $events; public function setEventManager(EventCollection $events) { $this->events = $events; } public function events() { if (!$this->events) { $this->setEventManager(new EventManager( array(__CLASS__, get_called_class()) ); } return $this->events; } public function do($foo, $baz) { $params = compact('foo', 'baz'); $this->events()->trigger(__FUNCTION__, $this, $params); } } $example = new Example(); $example->events()->attach('do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); }); $example->do('bar', 'bat'); 


This example essentially does the same thing as the first. The main difference is that the second argument of the trigger() method we pass to the handler context (the object - which started the processing of this event), and the handler receives it through the $e->getTarget() method - and can do something with it (within a reasonable :)).

You may have 2 questions:

Answer further.

EventCollection vs EventManager


One of the principles that ZF2 is trying to follow is the Liskov substitution principle . The interpretation of this principle may be the following: For any class that in the future may need to be redefined by another class, a “basic” interface must be defined. And it allows developers to use another implementation of a class by defining the methods of this interface.

Therefore, an EventCollection interface was developed that describes an object capable of aggregating listeners into events and initiating these events. EventManager is a standard implementation that will go into ZF2.

StaticEventManager


One aspect that the EventManager implementation provides is the ability to interact with the StaticEventCollection . This class allows you to attach handlers not only to named events, but also to events initiated by a specific context or purpose. EventManager , when processing events, also takes event handlers (subscribed to the current context) from the StaticEventCollection object and executes them.

How it works?

 use Zend\EventManager\StaticEventManager; $events = StaticEventManager::getInstance(); $events->attach('Example', 'do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); }); 


This example is almost identical to the previous one. The only difference is that the first argument in the attach() method, we pass the context - 'Example', to which we want to attach our handler. In other words, when processing the 'do' event, if this event is triggered by the context of 'Example', then we call our handler.

This is where the EventManager constructor EventManager play a role. The constructor allows you to pass a string, or an array of strings, specifying the name / names of contexts for which you need to take event handlers from the StaticEventManager . If an array of contexts is passed, then all event handlers from these contexts will be executed. Event handlers attached directly to the EventManager will be executed before the handlers defined in the StaticEventManager .

Let us combine the definition of the class Example and the static event handler from the last 2 examples, and add the following:

 $example = new Example(); $example->do('bar', 'bat'); 


At the output we get:
  Handled event "do" on target "Example", with parameters {"foo": "bar", "baz": "bat"} 

And now let's expand the class Example:

 class SubExample extends Example { } 


Notice which parameters we pass to the EventManager constructor - this is an array of __CLASS__ and get_called_class() . This means that when you call the do() method of the SubExample class, our event handler will also execute. If we specified only 'SubExample' in the constructor, then our handler will be executed only with SubExample::do() , but not with Example::do() .

Names used as contexts or goals need not be class names; you can use arbitrary names. For example, if you have a set of classes responsible for Caching or Logging, you can name the contexts as “log” and “cache”, and use these names, rather than class names.

If you do not want Event Manager to handle static events, you can pass the null parameter to the setStaticConnections() method:

 $events->setStaticConnections(null); 


In order to connect back the handling of static events:

 $events->setStaticConnections(StaticEventManager::getInstance()); 


Listener Aggregates


You may need to sign an entire class to handle several events, and in this “class handler” define methods for handling some events. To do this, you can implement the HandlerAggregate interface in your handler class. This interface defines 2 methods of attach(EventCollection $events) and detach(EventCollection $events) .

(I did not understand what I translated, the example below is more understandable).

 use Zend\EventManager\Event, Zend\EventManager\EventCollection, Zend\EventManager\HandlerAggregate, Zend\Log\Logger; class LogEvents implements HandlerAggregate { protected $handlers = array(); protected $log; public function __construct(Logger $log) { $this->log = $log; } public function attach(EventCollection $events) { $this->handlers[] = $events->attach('do', array($this, 'log')); $this->handlers[] = $events->attach('doSomethingElse', array($this, 'log')); } public function detach(EventCollection $events) { foreach ($this->handlers as $key => $handler) { $events->detach($handler); unset($this->handlers[$key]; } $this->handlers = array(); } public function log(Event $e) { $event = $e->getName(); $params = $e->getParams(); $log->info(sprintf('%s: %s', $event, json_encode($params))); } } 


To add such a handler to the event manager, use:

 $doLog = new LogEvents($logger); $events->attachAggregate($doLog); 


and any event that our handler ( LogEvents ) has to handle will be processed by the corresponding class method. This allows you to define “complex” event handlers in one place (stateful handlers).

Pay attention to the detach() method. Just like attach() , it takes an EventManager object as an argument, and “detaches” all handlers (from our array of handlers - $this->handlers[] ) from the event manager. This is possible because EventManager::attach() returns an object representing the handler - which we 'attached' earlier in the LogEvents::attach() method.

Handler Result


You may need to get the result of executing the event handlers. It should be borne in mind that several event handlers may be signed for one event. And the result of each handler should not conflict with the results of other handlers.

EventManager returns a ResponseCollection object. This class is inherited from the SplStack class, and gives you access to the results of all handlers (the result of the last handler will be at the beginning of the result stack).

ResponseCollection addition to the SplStack methods, SplStack has additional methods:

As a rule, when initiating event handling, you should not be strongly dependent on the output of the handlers. Moreover, when initiating an event, you cannot always be sure which event handlers will be subscribed to this event (perhaps there will not be a single handler at all, and you will not get any result). However, you have the ability to interrupt the execution of handlers if the desired result is already obtained in one of the handlers.

Interrupt event handling


If one of the handlers received a result that the initiator of the event expected; or the handler suddenly decides that something is going wrong; or one of the handlers, for some reason, decides that no further handlers are needed - you have a mechanism to interrupt the execution of the 'stack' of event handlers.

An example of where this might be needed is a caching mechanism built on the basis of an EventManager . At the beginning of your method, you initiate a “search data in cache” event, and if one of the handlers finds the necessary data in the responsible cache, the rest of the handlers are interrupted, and you return the data obtained from the cache. If it does not, then you generate the data, and trigger the “write to cache” event.

EventManager provides two ways to do this. The first way is to use a special method triggerUntil() , which checks the result of each handler executed, and if the result meets certain requirements, then the execution of subsequent handlers is interrupted.

Example:

 public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria2'); $results = $this->events()->triggerUntil(__FUNCTION__, $this, $params, function ($r) { return ($r instanceof SomeResultClass); }); if ($results->stopped()) { return $results->last(); } // ... do some work ... } 


The arguments to the triggerUntil() method are similar to the arguments to the trigger() method, with the exception of the optional argument at the end - the callback function, which checks the result of each handler, and if it returns true , then the subsequent handlers are interrupted.

Following this method, we know that the probable cause of the interruption of event processing is that the result of the work of the last executed handler satisfies our criteria.

Another way to interrupt event handling is to use the stopPropagation(true) method in the body of the handler itself. What will cause the event manager to stop the execution of subsequent handlers.

 $events->attach('do', function ($e) { $e->stopPropagation(); return new SomeResultClass(); }); 


With this approach, you can no longer be sure that event handling was interrupted due to the fact that the handler’s last result matches our criteria.

Handler execution order


You may want to specify the order of execution of the handlers. For example, you want the handler who maintains records in Log to execute guaranteedly, despite the fact that other handlers can interrupt the processing of this event at any time. Or when implementing Caching: a handler that searches in the cache was executed earlier than others; and the cache handler, on the contrary, was executed later.

EventManager::attach() and StaticEventManager::attach() have an optional priority argument (it defaults to 1) with which you can control the priority of the execution of handlers. A handler with a higher priority is executed before handlers with a lower priority.

 $priority = 100; $events->attach('Example', 'do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); }, $priority); 


Matthew Weier O'Phinney recommends using priorities only when absolutely necessary. And I probably agree with him.

Putting it all together: A simple caching mechanism


In the previous sections, it was written that using events and interrupting the processing of these is an interesting way to implement a caching mechanism in an application. Let's create a complete example of this.

First we define a method that could use caching.

Matthew Weier O'Phinney often uses __FUNCTION__ as an event name in his examples, and considers it a good practice because it allows you to easily write a macro to trigger events, and also allows you to uniquely determine the uniqueness of these names (especially as the context is usually the event). And for the separation of events caused by one method, use the postfixes like “do.pre”, “do.post”, “do.error”, etc.

In addition, $params passed to the event is a list of arguments passed to the method. This is because the arguments may not be stored in the object, and handlers may not get the parameters they need from the context. But the question remains, what is the name of the result parameter for the event that writes to the cache? The example uses __RESULT__ , which is convenient, since double underlining on both sides is usually reserved by the system.

Our method might look something like this:

 public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria1'); $results = $this->events()->triggerUntil(__FUNCTION__ . '.pre', $this, $params, function ($r) { return ($r instanceof SomeResultClass); }); if ($results->stopped()) { return $results->last(); } // ... do some work ... $params['__RESULT__'] = $calculatedResult; $this->events()->trigger(__FUNCTION__ . '.post', $this, $params); return $calculatedResult; } 


Now we define event handlers that work with the cache. We need to attach handlers to the 'someExpensiveCall.pre' and 'someExpensiveCall.post' events. In the first case, if the data is found in the cache, we return it. In the latter, we save the data to the cache.

We also assume that the $cache variable is defined earlier, and is similar to the Zend_Cache object. For the 'someExpensiveCall.pre' handler, we set the priority to 100 to ensure that the handler is faster than others, and for 'someExpensiveCall.post', the priority is -100, in case other handlers want to modify the data before writing to the cache.

 $events->attach('someExpensiveCall.pre', function($e) use ($cache) { $params = $e->getParams(); $key = md5(json_encode($params)); $hit = $cache->load($key); return $hit; }, 100); $events->attach('someExpensiveCall.post', function($e) use ($cache) { $params = $e->getParams(); $result = $params['__RESULT__']; unset($params['__RESULT__']); $key = md5(json_encode($params)); $cache->save($result, $key); }, -100); 

Note: we could define HandlerAggregate , and store $cache in a class property, and not import it into an anonymous function.


Of course, we could implement a caching mechanism in the object itself, and not put it into an event handler. But this approach gives us the ability to connect caching handlers to other events (implement a caching mechanism for other classes, storing the sampling logic from the cache and saving to the cache in one place), or attach other handlers to these events (which would deal with for example logging, or validation). The fact is that if you design your class using events, you make it more flexible and extensible.

Conclusion


EventManager is a new and powerful addition to the Zend Framework. It is already being used with the new MVC prototype to expand the capabilities of some of its aspects. After the release of ZF2 event model, I am sure, will be very much in demand.

Of course, there are some rough edges, which the people are working to eliminate.

From myself I will add - there is nothing cardinally new, it is nice that such a thing will appear in Zende - I will definitely use it.
I think the text is saturated with terms and hard to read (partly due to the lack of experience in translating articles).
I have nothing against criticism.

Original: http://weierophinney.net/matthew .

Source: https://habr.com/ru/post/131077/


All Articles