Logic before "dispatch"-event - php

I want to do some logic before MvcEvent::EVENT_DISPATCH, but MvcEvent::getTarget() function returns object of Mvc\Application instead of Controller if I set priority above 1 like:
$e->getApplication()->getEventManager()->attach(MvcEvent::EVENT_DISPATCH, array($this, 'routing'), 100);
If I set priority to negative value, I get the Controller object, but it's fired after action-function. How can I get the Controller object in this case?

you can either use MvcEvent::getRouteMatch() to determine what controller will be dispatched (if any), or attach to the controller event manager inside the controller itself by overriding AbstractActionController:setEventManager().
ie:
// in your controller
public function setEventManager(EventManagerInterface $events)
{
parent::setEventManager($events);
$events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'preDispatch'), 100);
}
public function preDispatch(MvcEvent $event)
{
// your custom logic here
}

// in your Module
public function onBootstrap(MvcEvent $e)
{
$e->getApplication()
->getEventManager()
->getSharedManager()
->attach(
'Zend\Stdlib\DispatchableInterface',
MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'),
2
);
}
public function onDispatch(MvcEvent $a)
{
$target = $a->getTarget();
}

Related

Cakephp 3 redirect in beforeFilter of parent class

In our CakePHP 3 application we found a different behaviour. We're sure that it worked well in CakePHP 2, so I suppose something changed in new version.
When user visits this url: /b2controller/myMethod, these methods run:
AppController::beforeFilter()
BController::beforeFilter()
B2Controller::beforeFilter()
B2Controller::myMethod()
B2Controller::myMethod2()
then user is redirected to this url /ccontroller/myMethod10/
But we need this:
When user visits
/b2controller/myMethod and $isOk condition is true, then redirect user to /ccontroller/myMethod10/, without running BController::beforeFilter(), B2Controller::beforeFilter(), B2Controller::myMethod() and BController::MyMethod2().
Our minimal code is like this:
class AppController {
function beforeFilter(Event $event) {
// set $isOk variable
if ($isOk == TRUE) {
return $this->redirect('/ccontroller/myMethod10/');
}
$aa=1;
$ab=2;
}
}
class BController extends AppController {
function beforeFilter(Event $event) {
parent::beforeFilter($event);
$a=1;
$b=2;
}
function myOtherMethod() {
myOtherMethod2();
}
function myOtherMethod2() {
...
...
}
}
class B2Controller extends BController {
function beforeFilter(Event $event) {
parent::beforeFilter($event);
$m1=1;
$m2=2;
}
function myMethod() {
myMethod2();
}
function myMethod2() {
...
...
}
}
class CController extends AppController {
function beforeFilter(Event $event) {
parent::beforeFilter($event);
}
function myMethod10() {
...
...
...
}
}
How can I make user to redirect to another controller action, from the beforeFilter of main class ? Note that redirect occurs. But user is redirected after calling myMethod() and myMethod2().
Also note that there is other controllers like CController that uses beforeFilter redirect behaviour.
Here are 3 methods that works:
Method 1 - Override startupProcess in your controller(s)
Override the startupProcess method of AppController:
// In your AppController
public function startupProcess() {
// Compute $isOk
if ($isOk) {
return $this->redirect('/c/myMethod10');
}
return parent::startupProcess();
}
This is a short and quite clean method, so I would go for this one if you can. If this does not fit your needs, see below.
Note: If you use this method, your components may not be initialized when you compute $isOk since the initialization is done by parent::startupProcess.
Method 2 - Send the response from AppController:
One easy but not really clean way may be to send the response from AppController::beforeFilter:
public function beforeFilter(\Cake\Event\Event $event) {
// Compute $isOk
if ($isOk) {
$this->response = $this->redirect('/c/myMethod10');
$this->response->send();
die();
}
}
Method 3 - Use Dispatcher Filters
A more "clean" way would be to use Dispatcher Filters:
In src/Routing/Filter/RedirectFilter.php:
<?php
namespace App\Routing\Filter;
use Cake\Event\Event;
use Cake\Routing\DispatcherFilter;
class RedirectFilter extends DispatcherFilter {
public function beforeDispatch(Event $event) {
// Compute $isOk
if ($isOk) {
$response = $event->data['response'];
// The code bellow mainly comes from the source of Controller.php
$response->statusCode(302);
$response->location(\Cake\Routing\Router::url('/c/myMethod10', true));
return $response;
}
}
}
In config/bootstrap.php:
DispatcherFactory::add('Redirect');
And you can remove the redirection in your AppController. This may be the cleanest way if you are able to compute $isOk from the DispatcherFilter.
Note that if you have beforeRedirect event, these will not be triggered with this method.
Edit: This was my previous answer which does not work very well if you have multiple B-like controllers.
You need to return the Response object returned by $this->redirect(). One way of achieving this is by doing the following:
class BController extends AppController {
public function beforeFilter(\Cake\Event\Event $event) {
$result = parent::beforeFilter($event);
if ($result instanceof \Cake\Network\Response) {
return $result;
}
// Your stuff
}
}
The code bellow the if is executed only if there was no redirection (parent::beforeFilter($event) did not return a Response object).
Note: I do not know how you compute isOk, but be careful of infinite redirection loop if you call $this->redirect() when calling /ccontroller/mymethod10.

ZF2 - Trigger MvcEvent::EVENT_DISPATCH_ERROR from View Controller

I am using a Restful Controller and on certain conditions, I would like the trigger the MvcEvent::EVENT_DISPATCH_ERROR and stop execution of the controller immediately after. In my Module class, I have attached an event listener for this but I can't find a way to trigger it from the view controller.
My Module code is:
public function onBootstrap(MvcEvent $mvcEvent) {
$eventManager = $mvcEvent->getApplication()
->getEventManager();
$eventManager->attach(array(MvcEvent::EVENT_DISPATCH_ERROR, MvcEvent::EVENT_RENDER_ERROR), array($this, 'error'));
}
public function error(MvcEvent $mvcEvent) {
echo $mvcEvent->getError();
die();
}
and my Controller code is:
public function indexAction() {
$mvcEvent = $this->getEvent();
$mvcEvent->setError('test-error-code');
$mvcEvent->getTarget()->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $mvcEvent);
return;
}
I think the problem is that you're not attaching to the Application's sharedEventManager. You can also use the Controller's own Event Manager to trigger the event.
Try something like this:
Module.php
public function onBootstrap(MvcEvent $mvcEvent) {
$eventManager = $mvcEvent->getApplication()->getEventManager()->getSharedManager();
$eventManager->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'error'));
}
Controller
public function indexAction() {
$mvcEvent = $this->getEvent();
$mvcEvent->setError('test-error-code');
$this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $mvcEvent);
return;
}

ZF2 how to listen to the dispatch event of a specific controller

How can I listen to the dispatch event of a specific controller? At the moment I do the following:
Module.php
public function onBootstrap(EventInterface $event) {
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$serviceManager = $application->getServiceManager();
$eventManager->attach($serviceManager->get('MyListener'));
}
MyListener.php
class MyListener extends AbstractListenerAggregate {
public function attach(EventManagerInterface $eventManager) {
$this->listeners[] = $eventManager->attach(
MvcEvent::EVENT_DISPATCH, function($event) {
$this->setLayout($event);
}, 100
);
}
public function setLayout(EventInterface $event) {
$event->getViewModel()->setTemplate('mylayout');
}
}
This sets the layout for all controller dispatches. Now I want to set the layout only if the application dispatches a specific controller.
Like all Modules have an onBootstrap() method, all controllers extending AbstractController have an onDispatch() method.
Considering you want to apply a different layout for a single specific controller, you can simply do the following:
<?php
namespace MyModule\Controller;
use Zend\Mvc\Controller\AbstractActionController; // Or AbstractRestfulController or your own
use Zend\View\Model\ViewModel; // Or JsonModel or your own
use Zend\Mvc\MvcEvent;
class MyController extends AbstractActionController
{
public function onDispatch(MvcEvent $e)
{
$this -> layout('my-layout'); // The layout name has been declared somewhere in your config
return parent::onDispatch($e); // Get back to the usual dispatch process
}
// ... Your actions
}
You may do this for every controller that has a special layout. For those who don't, well, you don't have to write anything.
If you often need to change your layout (e.g. you have to handle not a single controller but several), you can attach an MvcEvent in your module.php to get your layout setting code in one place.
To keep things simple, I'm not using a custom listener here, but you may use one as well.
<?php
namespace MyModule;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e -> getApplication() -> getEventManager();
$eventManager -> attach(
MvcEvent::EVENT_DISPATCH,
// Add dispatch error event only if you want to change your layout in your error views. A few lines more are required in that case.
// MvcEvent::EVENT_DISPATCH | MvcEvent::EVENT_DISPATCH_ERROR
array($this, 'onDispatch'), // Callback defined as onDispatch() method on $this object
100 // Note that you don't have to set this parameter if you're managing layouts only
);
}
public function onDispatch(MvcEvent $e)
{
$routeMatch = $e -> getRouteMatch();
$routeParams = $routeMatch -> getParams();
switch ($routeParams['__CONTROLLER__']) {
// You may use $routeParams['controller'] if you need to check the Fully Qualified Class Name of your controller
case 'MyController':
$e -> getViewModel() -> setTemplate('my-first-layout');
break;
case 'OtherController':
$e -> getViewModel() -> setTemplate('my-other-layout');
break;
default:
// Ignore
break;
}
}
// Your other module methods...
}
You have to attach your event listener to the SharedEventManager and listen MvcEvent::EVENT_DISPATCH of the "Zend\Stdlib\DispatchableInterface" interface.
See an example:
$eventManager->getSharedManager()
->attach(
'Zend\Stdlib\DispatchableInterface',
MvcEvent::EVENT_DISPATCH,
$serviceManager->get('MyListener')
);
Within your listener you can get the instance of the target controller like so $controller = $event->getTarget();
So, eventually, the method "setLayout" may look like this:
public function setLayout(MvcEvent $event)
{
$controller = $event->getTarget();
if ($controller instanceof MyController)
{
$event->getViewModel()->setTemplate('mycontroller-layout');
}
}

ZF2 events for multiple modules

Currently I have an ZF2 application configured with the single module "application". I bootstrap the application an attach an event this way:
namespace Application;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap( MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach( $eventManager);
$this->initTracking( $e);
}
/**
* Initialises user tracking check
* #param MvcEvent $e
*/
public function initTracking( MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach( 'dispatch', function( $e){
$objTracking = new \Application\Event\Tracking( $e);
}, 200);
}
}
Now I need to create a new module "api", which should process only urls starting domain.com/api (I configure the router in "api" module config file to handle only such urls).
I bootstrap the "api" module the same way as "application" module, and I attach a dedicated event:
namespace Api;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap( MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach( $eventManager);
$this->initLogging( $e);
}
/**
* Initialises loggging
* #param MvcEvent $e
*/
public function initLogging( MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach( 'dispatch', function( $e){
$objLogger = new \Application\Event\Logging( $e);
}, 200);
}
}
What happens is that when I call domain.com/application - both modules are being initialised and events from both modules are being triggered. I need events to be triggered depending on the application which is dispatching the action.
How can I achieve that?
You are currently attaching the event listeners to the application event manager. This is a single event manager instance that will trigger all MVC events.
As it is the same instance it will make no difference where you attach the listeners; they will all be triggered regardless.
You will need to specifically check, in each listener, if the matched route is one that the listener should action. If it is not then exit out early.
For example:
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
// There is no need to pass in the event
// to a seperate function as we can just attach 'initLogging' here
// as the event listener
$eventManager->attach('dispatch', array($this, 'initLogging'), 200);
}
// initLogging listener
public function initLogging(MvcEvent $event)
{
//... check the route is one you want
// this is quite basic to you might need to edit to
// suit your specific needs
$routeName = $event->getRouteMatch()->getMatchedRouteName();
if (false === strpos($routeName, 'api')) {
// we are not an api route so exit early
return;
}
$objLogger = new \Application\Event\Logging($event);
}
So the listener will still be triggered, however it won't 'do' anything.
You can however go further and prevent this unnecessary call by specifically targeting the required event manager that you are interested in; to do so we can use the SharedEventManager.
When attaching the listener to the SharedEventManager you need to provide an 'identifier' of the target event manager - I'll assume you are targeting a 'API controller'.
So the above would be changed to
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$sharedEventManager = $application->getEventManager()
->getSharedManager();
// The shared event manager takes one additional argument,
// 'Api\Controller\Index' is our target identifier
$eventManager->attach('Api\Controller\Index', 'dispatch', array($this, 'initLogging'), 200);
}
// initLogging listener
public function initLogging(MvcEvent $event)
{
// ... same bits we had before
}
the onDispatch method will be run in only one module
namespace Application;
use Zend\Http\PhpEnvironment\Request;
use Zend\Http\PhpEnvironment\Response;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\ModuleManagerInterface;
use Zend\Mvc\MvcEvent;
/**
* #method Request getRequest()
* #method Response getResponse()
*/
class Module implements ConfigProviderInterface
{
public function getConfig()
{
return array_merge(
require __DIR__ . '/../config/module.config.php',
require __DIR__ . '/../config/router.config.php'
);
}
public function init(ModuleManagerInterface $manager)
{
$eventManager = $manager->getEventManager();
// Register the event listener method.
$sharedEventManager = $eventManager->getSharedManager();
$sharedEventManager->attach(__NAMESPACE__, MvcEvent::EVENT_DISPATCH,
[$this, 'onDispatch'], 100);
}
public function onDispatch(MvcEvent $e)
{
var_dump(__METHOD__);
}

Pass data to controller from Module.php

How can I pass data to controllers from Module class?
I need to pass data from onBootstrap method to all module controllers. What is the best way to do this. I can access controller using $e->getTarget() but don't know how to pass custom data to it. Maybe controller has storage for that?
The controller has access to the MvcEvent you can setup an event listener to attach arbitrary data to it and then fetch it within the controller.
Module.php
public function onBootstrap(MvcEvent $event)
{
$event->setParam('foo', 'bar');
}
Controller
public function fooAction() {
$foo = $this->getEvent()->getParam('foo', false);
}
#JonDay suggested an event listener which would also work well.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function($event) {
$controller = $event->getTarget();
// Set public property
$controller->foo = 'bar';
// OR protected with setter
$controller->setFoo('bar');
});
}

Categories