Zend Framework 2 : Getting Closer with EventManager
I have posted a dozen of posts that utilize EventManager, but i think, it’s time to post detail about EventManager itself. Zend Framework 2 has component named EventManager. An EventManager is an object that aggregates listeners for one or more named events, and which triggers events, which a Listener is a callback ( closure, static method, typical array callback ) that can react to an event, and An Event is an action that is triggered by an EventManager.
This is a simple example :
use Zend\EventManager\EventManager; $events = new EventManager; $events->attach('do', function($e) { $event = $e->getName(); $params = $e->getParams(); printf( 'Handled event "%s" with parameter "%s"', $event, json_encode($params) ); }); $params = array('foo' => 'bar','baz' => 'bat'); $events->trigger('do', null, $params); //event, target, parameter //print : Handled event "do" with parameter "{"foo":"bar","baz":"bat"}"
Features
- Wildcard Attachment
- Shared Manager
- Short-Circuiting
- Listener response Aggregation
Ok, let’s learn one by one.
1.Wildcard Attachment
We can attach to many events at once.
for example :
$events = new EventManager(); $events->attach(array('these', 'are', 'event', 'names'), $callback);
or using ‘*’ to make $callback available in all events.
$events = new EventManager(); $events->attach('*', $callback);
2.Shared Manager
This is to allow attaching to events when you don’t have access to the object. You should implements Zend\EventManager\EventManagerAwareInterface to make it run.
For example, we create a class like the following :
use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; class Foo implements EventManagerAwareInterface { protected $events; public function setEventManager(EventManagerInterface $events) { $this->events = $events; return $this; } public function getEventManager() { if (!$this->events) { $this->setEventManager(new EventManager(__CLASS__)); } return $this->events; } public function bar($baz, $bat = null) { $params = compact('baz', 'bat'); $this->getEventManager()->trigger(__FUNCTION__, $this, $params); } }
and you can attach to it by :
use Zend\EventManager\SharedEventManager; $sharedEvent = new SharedEventManager; $sharedEvent->attach('Foo', 'bar', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); $params = json_encode($e->getParams()); printf( '%s called on %s, using params %s', $event, $target, $params ); }); $foo = new Foo(); $foo->getEventManager()->setSharedManager($sharedEvent); $foo->bar('bazvalue', 'batvalue'); //print : bar called on Foo, using params {"baz":"bazvalue","bat":"batvalue"}
We can call other class too, let’s assume we have two class that had virtually no knowledge of each other.
Let’s create class 1 named Class1 :
use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; class Class1 implements EventManagerAwareInterface { protected $events; public function setEventManager(EventManagerInterface $events) { $this->events = $events; return $this; } public function getEventManager() { if (!$this->events) { $this->setEventManager(new EventManager(__CLASS__)); } return $this->events; } public function run() { $this->getEventManager()->trigger('cls',$this); } }
and class 2 named Class2 :
use Zend\EventManager\Event; class Class2 { public function listen(Event $e) { echo get_class($this) . ' has been called by ' . get_class($e->getTarget()); } }
Check it :
use Zend\EventManager\StaticEventManager; StaticEventManager::getInstance()->attach('Class1', 'cls', array(new Class2, 'listen')); $cls = new Class1(); $cls->run(); //print : Class2 has been called by Class1
3.Short-Circuiting
This feature utilize if a particular result is obtained, or if a listener determines that something is wrong, or that it can return something quicker than the target.
for example, we have a class FooShortCircuit :
use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; class FooShortCircuit implements EventManagerAwareInterface { protected $events; public function setEventManager(EventManagerInterface $events) { $this->events = $events; return $this; } public function getEventManager() { if (!$this->events) { $this->setEventManager(new EventManager(__CLASS__)); } return $this->events; } public function execute($obj) { $argv = array(); $results = $this->getEventManager()->triggerUntil(__FUNCTION__, $this, $argv, function($v) use ($obj) { return ($obj instanceof Foo); }); // if $obj instanceof foo, so stopped if ($results->stopped()) { return $results->last(); } // continue... echo 'hei, i\'m continue :p'; } }
if $obj in execute function instance of Foo class, it will stopped.
Let’s create a default execution :
$sharedEvent = new SharedEventManager; $sharedEvent->attach('FooShortCircuit', 'execute', function($e) { echo 'standard execution...'; });
if we pass new Foo() to execute() function, it should execute ‘standard execution…’ only, and stopped. Let’s check it :
$fooShortCircuit = new FooShortCircuit; $fooShortCircuit->getEventManager()->setSharedManager($sharedEvent); $fooShortCircuit->execute(new Foo()) ; //print : standard execution...
4.Listener response Aggregation
Single class can listen to multiple events.
For example, we have a class :
use Zend\EventManager\EventManager; use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerInterface; class Baz implements EventManagerAwareInterface { protected $events; public function setEventManager(EventManagerInterface $events) { $this->events = $events; return $this; } public function getEventManager() { if (!$this->events) { $this->setEventManager(new EventManager(__CLASS__)); } return $this->events; } public function get($id) { $params = compact('id'); $results = $this->getEventManager()->trigger('Bar.pre', $this, $params); // If an event stopped propagation, return the value if ($results->stopped()) { return $results->last(); } // do some work... $this->getEventManager()->trigger('Bar.post', $this, $params); } }
and make a class that listen to multiple events.
use Zend\EventManager\ListenerAggregateInterface; use Zend\EventManager\EventInterface; class Bar implements ListenerAggregateInterface { protected $listeners = array(); public function attach(EventManagerInterface $e) { $this->listeners[] = $e->attach('Bar.pre', array($this, 'load')); $this->listeners[] = $e->attach('Bar.post', array($this, 'save')); } public function detach(EventManagerInterface $e) { foreach ($this->listeners as $index => $listener) { if ($e->detach($listener)) { unset($this->listeners[$index]); } } } public function load(EventInterface $e) { echo 'load...'; } public function save(EventInterface $e) { echo 'save...'; } }
Let’s aggregate it :
$baz = new Baz; $barListeners = new Bar; $baz->getEventManager()->attachAggregate($barListeners); $baz->get(1);
EventManager in ZF2 MVC Architecture
So, how EventManager works in ZF2 MVC Architecture ? Everything is an event !
- A bootstrap is an Event
- A Routing is an Event
- A Dispatch is an Event
- A Rendering View is an Event
- etc
To make event triggered early, place it to init() function in Module.php
class Module { public function init(ModuleManager $m) { $event = $m->getEventManager()->getSharedManager(); $event->attach('Zend\Mvc\Application', 'bootstrap', function($e) { echo 'executed on bootstrap app process <br />'; }); } }
And if init() is too early, ModuleManager automatically registers the onBootstrap method if found in the module.
class Module { public function onBootstrap(MvcEvent $e) { $event = $e->getApplication()->getEventManager(); $event->attach('route', function($e) { echo 'executed on route process'; }); $event->attach('dispatch', function($e) { echo 'executed on dispatch process'; }); $event->attach('dispatch.error', function($e) { echo $e->getParam('exception'); echo 'executed only if found an error <br />, for ex : sm not found <br />'; }); $event->attach('render', function($e) { $e->getViewModel()->setVariable('test', 'test'); echo 'executed on render process <br />'; }); $event->attach('render.error', function($e) { echo 'executed on render error found'; }); $event->attach('finish', function($e) { echo 'executed on finish process'; }); } }
A sample Use Case
We want to check if logged in, setting up the userid variable of controllers.
class Module { public function onBootstrap(MvcEvent $e) { $event->getSharedManager() ->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function($e) { $controller = $e->getTarget(); //check if logged in, setting up the userid variable of controllers if ($e->getApplication()->getServiceManager()->get('AuthService') ->hasIdentity()) { $users = $e->getApplication()->getServiceManager() ->get('SanAuth\Model\AuthStorage')->read(); $controller->userid = $users['id']; } }, 100); } }
So Full ? make it separate…
class Module { public function onBootstrap(MvcEvent $e) { $event = $e->getApplication()->getEventManager(); $event->getSharedManager() ->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', array($this, 'settingUpControllerVariables'), 100 ); } public function settingUpControllerVariables(MvcEvent $e) { $controller = $e->getTarget(); //check if logged in, setting up the userid variable of controllers if ($e->getApplication()->getServiceManager() ->get('AuthService') ->hasIdentity()) { $users = $e->getApplication()->getServiceManager() ->get('SanAuth\Model\AuthStorage')->read(); $controller->userid = $users['id']; } } }
Hm…, i think it’s enough 😀
References :
- Zend Framework 2 Quick Start – Mathew Weier O’Phinney
- ZF2 Modules and Services – Maurice Kherlakian
- http://en.wikipedia.org/wiki/Event-driven_programming
- http://akrabat.com/zend-framework-2/an-introduction-to-zendeventmanager/
- http://zf2.readthedocs.org/en/latest/modules/zend.event-manager.event-manager.html
- http://www.mwop.net/blog/266-Using-the-ZF2-EventManager.html
- http://www.eschrade.com/page/a-little-more-advanced-zf2-eventmanager-usage/
- http://www.maltblue.com/tutorial/zend-framework-2-event-manager-a-gentle-introduction
[…] Zend Framework 2: Getting Closer With EventManager […]
Great article!
Are you going to make similar post about ServiceManager?
i’ve already wrote on this : https://samsonasik.wordpress.com/2013/01/02/zend-framework-2-cheat-sheet-service-manager/ please take a look 😉
dear samsonasik,
Thank you for your great article!
I have a question that I want to put to you.
Is there any relationship between setEventManager(), getControllerConfig() and OnBootstrap() ? Which of these is being called first? and then, and last?
Because I read an article at: http://www.mwop.net/blog/2012-07-30-the-new-init.html , see the author attach dispatch event here, in setEventManager() and in getControllerConfig(), and we can also attach dispatch in OnBootstrap(), so I’m wondering which has the high priority?
Thank you!
it should be based on the priority, if priority is negative, it should be executed after action dispatched. it based on event you used too. if you place it at ‘route’ event, it should be executed before action dispatched.
Hi samsonasik,
We register to listen to ‘dispatch’ event onBootstrap() function. In this case, it will check authentication is valid or not. Is it OK if I put all check-authentication-code in onDispatch() function like ZF 1? I think It should be run because Zend\Mvc\AbstractController has already register to listen to ‘dispatch’ event which is triggered by Application class. When this event triggers, onDispatch() function in AbstractController should be called. Do you have any idea?
Thank you!
I think what you mean is authorization. if yes, the answer is “No”. use at ‘route’ event, it should executed before dispatched action for security reason.
Thanks for your answer! It helps me understand more about ‘route’ event.
Correct me if I’m wrong. (And sorry for my English if there is anything make you confusing).
I have some ideas to make thing clearer.
We have 2 jobs here. Check for ACL and check for already logging or not.
Sometime, user don’t need to log in to visit our site, we call that ‘anonymous’ and of course, their rights are restricted. If they’ve already logged in, their right will change. We do this functionality with ‘route’ event. (like the comment above).
But before that we have to check if user log in or not in some page that user have to login before visiting. We do this functionality with ‘dispatch’ event, right? In your article, you attach such that code in OnBootstrap() function in Module.php file. It’s OK. Code following:
$event->getSharedManager()
->attach(‘Zend\Mvc\Controller\AbstractActionController’,
‘dispatch’,
function($e) {
$controller = $e->getTarget();
//check if logged in, setting up the userid variable of controllers
if ($e->getApplication()->getServiceManager()->get(‘AuthService’)
->hasIdentity()) {
$users = $e->getApplication()->getServiceManager()
->get(‘SanAuth\Model\AuthStorage’)->read();
$controller->userid = $users[‘id’];
}
});
}
My question is: Can I override the function onDispatch() of AbstractActionController Class like this:
public function onDispatch(MvcEvent $e) {
if ($e->getApplication()->getServiceManager()->get(‘AuthService’)
->hasIdentity()) {
$users = $e->getApplication()->getServiceManager()
->get(‘SanAuth\Model\AuthStorage’)->read();
$controller->userid = $users[‘id’];
}
}
like ZF 1.
If so, is there any problem with that way?
Thank you so much!
if i’m not missunderstand your question. yes, you can. use something like this :
and No. i’m the one that poor in English. so pardon me 😉
Thank you!
I’ve just run it. I works well. Just one thing more. what do we do at ‘//do something after dispatch…’ above?
Thank you again. 🙂 You always have good articles about Zend 2. I’m working on Zend 2 project now and you help me out a lot through series of your article.
you’re welcome. it means, if you want to execute something that happen before view rendered, but after action code ( before return viewmodel), you can place on it.
Thanks!
you’re welcome 😉
Sorry to bother you, samsonasik,
I follow you instruction in https://samsonasik.wordpress.com/2012/08/23/zend-framework-2-controllerpluginmanager-append-controller-pluginto-all-controller/, I add redirection in else condition if hasIdentity() failed. Like the following:
if($e->getApplication()->getServiceManager()
->get(‘AuthService’)->hasIdentity())
{
$users = $e->getApplication()->getServiceManager()->get(‘User\Model\TtkAuthStorage’)->read();
}
else
{
$url = $e->getRouter()->assemble(array(‘controller’=>’auth’, ‘action’=>’index’), array(‘name’ => ‘user/process’));
$response = $e->getResponse();
$response->setStatusCode(302);
$response->getHeaders()->addHeaderLine(‘Location’, $url);
$e->stopPropagation(true);
return $response;
}
But It acts weirdly. http://zf2-dev.localhost/user/auth/login don’t redirect.
The browser says: Firefox has detected that the server is redirecting the request for this address in a way that will never complete.
Could you help me?
Thank you!
check your route !
Not make sense, samsonasik.
I guest the problem here is: when we redirect to user/auth/login, it still catches ‘dispatch’ event. Because hasIdentity() still returns false so we redirect to user/auth/login repeatedly. We don’t have code to check if it’s already in user/auth/login, stop check for hasIdentity(). Let me give it a try!
Thank you!
When I’m using:
$users = $e->getApplication()->getServiceManager()
->get(‘Zend\Authentication\Storage\Session’)->read();
I get error:
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Zend\Authentication\Storage\Session
Any idea why is that?
not every component registered in ZF2 ServiceManager. extends and register yourself first.
Hi samsonasik,
Is it possible If I load a module on my own, not let the module name in application.config.php file. In fact I want store all modules name in database.
yes, just change in index.php :
with something like this :
Thank you million times!
you’re welcome 😉
I’ve tried to do lot of things like: catch ‘loadmodules’ event, get ModuleManager and then call its loadModule() function but it all does not make sense. In daily work, I usually get Database Adapter via Service Manager, so I think we can not get Adapter at the very early time before the Application bootstrap so that we have not had Service Manager yet. Your way is the key I need. Thank you!
Dear Samsonasik,
I try to code my project example with your tutorial above, but I simple assign variable to controller like that.
class Module
{
public function onBootstrap(MvcEvent $e)
{
$e->getApplication()
->getEventManager()
->getSharedManager()
->attach(‘Zend\Mvc\Controller\AbstractActionController’, ‘dispatch’, function($e)
{
$controller = $e->getTarget();
$controller->test = ‘hello world’;
}
);
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
}
And in my IndexController, indexAction, I wanna get ‘test’ has been assigned, but I cannot get it, it return NULL. Below that’s my code:
class IndexController extends AbstractActionController
{
public function indexAction()
{
var_dump($this->test);
return new ViewModel();
}
}
And result display on browser
Notice: Undefined property: Application\Controller\IndexController::$test
NULL
What’s wrong with my code. Can you help me or suggestion.
Thanks and best regard,
Ooops, i forgot to mention a priority. pass a priority to attaching event.
post updated.
Thanks Samsonasik, I done it. By the way, let me ask “why do you choose priority = 100? What else priority?”
priority is positive and negative, you can set as your own, 999 maybe. higher, will be serve first.
Thanks your explaination.
you’re welcome 😉
@samsonasik you are awesome, it seems that you are helping all people here, ZF2 lucky that such person like you write an articles about it 🙂
thanks 😉
@samsonasik: thanks from my side too! i found a lot of good hints in your posts around here that finally initiated the *click* in my head.
Keep it up – already bookmarked this blog 😉
you’re welcome ;). Yes, I will 😉
[…] Zend Framework 2: Getting Closer With EventManager […]
@samsonasik, thanks for the article.
A quick question. If one attaches events inside onBootstrap without using the SharedManager, does it only apply to MvcEvents, i.e. route, dispatch, dispatch.error etc?
no. as far the class implement EventManagerAwareInterface, you can do it to attach other class.
great article!!helped me understand event manger better..thnx 🙂
you’re welcome 😉
[…] https://samsonasik.wordpress.com/2013/03/30/zend-framework-2-getting-closer-with-eventmanager/ […]
Great work! Now how about an event that is triggered on all events in a single Module. Something like a front controller plugin. Because everything I put in any module.php gets trigered across the entire application, all modules.
Hi Samsonasik,
Can you help me get Service Locator when attach Event.
My code is below
class Attributes implements ListenerAggregateInterface
{
protected $_service;
protected $_listeners = array();
public function __construct(ServiceLocatorInterface $sl)
{
$this->_service = $sl;
}
public function attach(EventManagerInterface $events)
{
$anotherService = $this->_service->get(‘AnotherService’);
// Listener for event add new attribute
$this->_listeners[] = $events
->getSharedManager()
->attach(‘Background\Model\AbstractModel’, ‘addNewAttribute.post’, function ($e) use ($anotherService)
{
/* Execute event */
}, -10);
}
public function detach(EventManagerInterface $events)
{
foreach ($this->_listeners as $index => $listener)
{
if ($events->detach($listener))
{
unset($this->_listeners[$index]);
}
}
}
}
In lambda function, can I use $e to get Service Locator. Thanks so much
the easiest way to pass $this->_service that you’ve assign at __construct.
That means Event Object cann’t get Service Locator directly?
as far as $e represent MvcEvent, then we can call via $e->getTarget()->getServiceLocator() or $e->getApplication()->getServiceLocator(). we often see it at Module::onBootstrap. so the answer is depend $e represent of.
Thanks so much Samsonasik, I got it. 🙂
you’re welcome
terima kasih pak atas pencerahannya 😀
ya 😉
Dear Samsonasik,
Can you help me to explain: what is “callback” parameter (4th parameter) in trigger function? When use it? Can you show me example for it?
Thank so lot.
callback on this case is a term for something that react to event, it can be closure, static method, or typical array callback
I want to access this $controller->userid outside means layout.phtml file. but its value cant get why?
you can set controller var via layout with
and got it from layout :
I am using this method
public function onBootstrap(MvcEvent $e)
{
$event = $e->getApplication()->getEventManager();
$event->getSharedManager()
->attach(‘Zend\Mvc\Controller\AbstractActionController’,
‘dispatch’,
array($this, ‘settingUpControllerVariables’), 100
);
}
public function settingUpControllerVariables(MvcEvent $e)
{
$controller = $e->getTarget();
//check if logged in, setting up the userid variable of controllers
if ($e->getApplication()->getServiceManager()
->get(‘AuthService’)
->hasIdentity()) {
$users = $e->getApplication()->getServiceManager()
->get(‘SanAuth\Model\AuthStorage’)->read();
$controller->userid = $users[‘id’];
$e->getViewModel()->setVariable(‘user_data’, $controller->userid);
}
}
I want to this $controller->userid outside means my layout file and also in my footer file. Thanks in Advance.
read https://samsonasik.wordpress.com/2012/07/27/zend-framework-2-mvcevent-layout-view-get-namespace/
[…] https://samsonasik.wordpress.com/2013/03/30/zend-framework-2-getting-closer-with-eventmanager/ […]
Hi, if you often use ListenerAggregates you could extend Zend\EventManager\AbstractListenerAggregate
thanks
i need to load module from another module
Say i have Modules A,B
how to load module “B” From Module “A” but not through application.config.php
this can help you https://github.com/zendframework/zf2/blob/bbb0545ad6552b9ecadd96c12223022b38734686/tests/ZendTest/ModuleManager/TestAsset/LoadOtherModule/Module.php
i used it its good but the problem when i use it from init the module config.php not loaded or not merged to other config
say i add a route or set the template_path_stack value will not be loaded is this a bug or i am missing something
you can use ModuleEvent::EVENT_MERGE_CONFIG, please read http://zf2.readthedocs.org/en/latest/tutorials/config.advanced.html#manipulating-merged-configuration
samsonasik,
Greate article about event managers
I read MANUAL and I found Modifying Arguments, I understand that is good Idea, but i don’t understand how i can use it
Hello Samsonasik,
Stuck in a problem where I would like to append a query string such as ?id=somestring to each and every url. There are around 150 to 200 urls in application. So Is there a way to do it using event (route).
Waiting for your reply.
probably you could do this :