ZF2 events for multiple modules - php

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__);
}

Related

Adding events to Laminas

I am trying to add event for Laminas Framework that will fire when \Laminas\Mvc\MvcEvent::EVENT_DISPATCH is triggered. But absolutelly nothing happends, like this triggers not exists. What am I doing wrong?
This is the code under the module\Application\src\Module.php:
use Laminas\ModuleManager\ModuleManager;
use Laminas\Mvc\MvcEvent;
class Module
{
public function init(ModuleManager $moduleManager)
{
ini_set("display_errors", '1');
$eventManager = $moduleManager->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']);
}
public function onDispatch(\Laminas\EventManager\Event $event)
{
var_dump('ok');die;
}
}
I think you need use another method in Module it's should be something like this:
use Laminas\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']);
}
public function onDispatch(MvcEvent $event)
{
var_dump('ok');
die;
}
}
In this case it onBootstrap. Hope help you
On init you'll need to get the shared event manager from the module manager:
<?php
use Laminas\ModuleManager\Feature\InitProviderInterface;
use Laminas\ModuleManager\ModuleManagerInterface;
use Laminas\Mvc\Application;
use Laminas\Mvc\MvcEvent;
final class Module implements InitProviderInterface
{
public function init(ModuleManagerInterface $manager): void
{
$sharedEventManager = $manager->getEventManager()->getSharedManager();
$sharedEventManager->attach(
Application::class,
MvcEvent::EVENT_DISPATCH,
function () {
var_Dump('dispatch from init');
}
);
}
}
The SharedEventManager is usually (or should be) shared between all event manager instances. This makes it possible to call or create events from other event manager instances. To differentiate between event names an identifier is used (so you can have more then one event with the same name). All MvcEvents belong to the Laminas\Mvc\Application identifier. Laminas\ModuleManager\ModuleManager has it's own EventManager instance, that is why you'll need to add the event to the SharedEventManager (init() is called by the ModuleManager and Laminas\ModuleManager\ModuleEvent is used).
onBootstrap() will be called by Laminas\Mvc\Application, that why you get the correct EventManager instance there.
As #Dimitry suggested: you should add that event in onBootstrap() as the dispatching process is part of the application and not the module manager. In init() you should only add bootstrap events.
And btw: you should use the Laminas\ModuleManager\Feature\* interfaces to make your application a bit more robust to future updates.

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');
}
}

Change the DefaultEntityListenerResolver on Doctrine with ZF2

I am trying to change the Doctrine DefaultEntityListenerResolver without success.
I have the need to call the service manager inside EntityListener defined trought annotation #ORM\EntityListeners.
So I've coded this ListenerResolver to check if a Listener implements the ServiceLocatorAwareInterface:
class ListenerResolver extends DefaultEntityListenerResolver {
private $serviceManager;
public function __construct(ServiceLocatorInterface $serviceManager){
$this->serviceManager = $serviceManager;
}
public function resolve($className){
$listener = parent::resolve($className);
if ($listener instanceof ServiceLocatorAwareInterface){
$listener->setServiceLocator($this->serviceLocator);
}
return $listener;
}
}
And to change the Listener Resolver I've created a bootstrap function on my Module.php to change the resolver on DoctrineORMModule:
class Module {
public function onBootstrap(MvcEvent $e){
$serviceManager = $e->getTarget()->getServiceManager();
$resolver = new ListenerResolver($serviceManager);
$serviceManager->get('doctrine.entitymanager.orm_default')->getConfiguration()->setEntityListenerResolver($resolver);
}}
But I still can't reach the Service Manager, any suggestion?
The point is that we don't really need to change the EntityListenerResolver, we only need to register a new behavior. So I registered the listener at the current EntityListenerResolver on the bootstrap.
1- Implement the ServiceLocatorAwareInterface on each entity listener.
namespace Application\Business\Entity\Listener;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
class User implements ServiceLocatorAwareInterface {
}
2 - Register the entity listeners as Invokables on Module.php.
public function getServiceConfig()
{
return array(
'invokables'=>array(
//EntityListeners
'Application\Business\Entity\Listener\User'=>'Application\Business\Entity\Listener\User',
),
)
)
)
}
3 - Register the listeners at onBootstrap on Module.php, I did that by iterating over the list of invokables and search for the listeners namespaces.
public function onBootstrap(MvcEvent $e){
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
//Inejct the Service Maganer on listeners
$serviceManager = $e->getApplication()->getServiceManager();
$entityManager = $serviceManager->get('Doctrine\ORM\EntityManager');
//Register the Entity Listeners
$config = $this->getServiceConfig();
$invokables = $config['invokables'];
foreach ($invokables as $invokable){ //Verify the listener namespaces
if((strpos($invokable, 'Application\Business\Entity') !== FALSE) &&(strpos($invokable, '\Listener') !== FALSE)){
$entityManager->getConfiguration()->getEntityListenerResolver()->register($serviceManager->get($invokable));
}
}
}
}
And you are done... You can now freely use $this->getServiceLocator()->get() on listeners...
Hope it helps..

Plugin for zend framework

$url = $_SERVER['SERVER_NAME'];
if(!filter_var($url, FILTER_VALIDATE_URL)){
return false;
}
return true;I need to connect plugin CheckDomain which loaded on pre dispatch with all modules except Admin.
Plugin is a class CheckDomain which could be called as a function CheckDomain() when it's called in that way it checks is domain equal to "test.example.com"
<?php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\Mvc\Controller\Plugin\FlashMessenger;
use Zend\Mvc\Controller\Plugin\Forward;
use Zend\Mvc\Controller\Plugin\Layout;
use Zend\Mvc\Controller\Plugin\Params;
use Zend\Mvc\Controller\Plugin\PostRedirectGet;
use Zend\Mvc\Controller\Plugin\Redirect;
use Zend\Mvc\Controller\Plugin\Url;
use Zend\View\Model\ViewModel;
class CheckDomainPlugin extends AbstractPlugin{
public function checkdomain()
{
$url = $_SERVER['SERVER_NAME'];
if(!filter_var($url, FILTER_VALIDATE_URL)){
return false;
}
return true;
}
}
I call it for every controller except Admin, but I need to use it once.
I mean is it possible to load automatically plugin for all modules axcept admin
if you wish to "hook up" into every module you should read the zf2 documentation about MVC Events and the EventManager class.
http://framework.zend.com/manual/2.3/en/modules/zend.mvc.mvc-event.html#the-mvcevent
http://framework.zend.com/manual/2.3/en/modules/zend.event-manager.event-manager.html
Here is a small example for your Application/Module.php
public function onBootstrap(MvcEvent $e)
{
$application = $e->getApplication();
$serviceManager = $application->getServiceManager();
$eventManager = $application->getEventManager();
$sharedManager = $eventManager->getSharedManager();
// DISPATCH EVENT
$sharedManager->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function( MvcEvent $e) use ($serviceManager) {
$controller = $e->getTarget();
$controllerClass = get_class($controller);
$moduleNamespace = substr($controllerClass, 0, strpos($controllerClass, '\\'));
// this is the first segment from the module namespace
// if the Admin Namespace is something like this Admin/Controller/...
if( $moduleNamespace != 'Admin' ) {
$CheckDomainPlugin = $serviceManager->get('ControllerPluginManager')->get('CheckDomainPlugin');
// do something
}
}, 50 );
}
If you create a plugin, or any service, they will be available to all modules within the application. If there are areas in which the plugin should not be used then don't call it!
If I actually understand your problem; I would use an event listener for this. If you listen 'on dispatch' you can exclude all the admin controllers if you give them a unique interface.
// Module.php
public function onBootstrap($event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach(
'Zend\Mvc\Controller\AbstractActionController',
'dispatch',
function($e) {
$target = $e->getTarget(); // The dispatched controller
if ($controller instanceof AdminControllerInterface) {
return;
}
// Do something here
}
);
}

Modify ViewModel after controller dispatch

I'm trying to intercept the ViewModel, prior to it being rendered, and add it to another 'parent' view model - much like the way that the ZF2 layout wraps around the controllers returned content.
The simple (working) way to do this would be in each controller action.
public function dashboardAction() {
$dashboardContent = new ViewModel(array('foo' => 'bar'));
$parent = new ViewModel();
$parent->setTemplate('foo/bar/parent-template');
$parent->addChild($dashboardContent, 'content');
return $parent;
}
This works as expected and the 'child' view is correctly nested within the 'parent' in the final output.
As I have a number of controllers/actions that should all behave in the same way (resolved via their route name); I was hoping to encapsulate this in an event listener.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'addUserAccountLayout'), -100);
}
public function addUserAccountLayout(EventInterface $event)
{
$routeMatch = $event->getRouteMatch();
$controller = $event->getTarget();
$result = $event->getResult();
if (! $result instanceof ViewModel || $result instanceof JsonModel) {
return;
}
if (! $routeMatch instanceof RouteMatch || 0 !== strpos($routeMatch->getMatchedRouteName(), 'zfcuser') || $result->terminate()) {
return;
}
$application = $event->getApplication();
$serviceManager = $application->getServiceManager();
$accountService = $serviceManager->get('JobboardUser\Service\AccountService');
$user = $accountService->getCurrentUser();
$parent = new ViewModel();
$parent->setVariables(compact('user'));
$parent->setTemplate('jobboard-user/widget/user-account');
$parent->addChild($result, 'content');
$event->setResult($parent);
}
This however is not working; the normal view is rendered (without the parent). My guess is because I am either not using the correct event or event priority OR $event->setResult($view) is not the correct way to assign the result.
How can I modify and then re-assign the view from within an event listener?
I can't offer an explanation as to 'why', but a priority of -10 seems to be the sweet spot to get this working with the code you already have.
I do have one suggestion though, instead of listening to every dispatch event triggered by every module controller and then having to check if it's a zfcuser route, you can instead make use of the shared events manager to listen specifically to the ZfcUser controller you're interested in ...
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$sharedEvents = $eventManager->getSharedManager();
$sharedEvents->attach(
'ZfcUser\Controller\UserController', // controller FQCN to listen to
MvcEvent::EVENT_DISPATCH,
array($this, 'addUserAccountLayout'),
-10
);
}
If you do that, you can remove the routematch check 0 !== strpos($routeMatch->getMatchedRouteName(), 'zfcuser') in your callback entirely.
It's been a little while since I asked this; however I did find the solution I was after. It was indeed related to how the new ViewModel is attached back to the MvcEvent.
Previously I had tried to set the event's result property with no luck :
$mvcEvent->setResult($myNewViewModel);
The key was to first fetch the MvcEvent's own view model and then attach my custom one to it as a child.
$mvcEvent->getViewModel()->addChild($myNewViewModel, 'content');
I've also incorporated the point that #Crisp made regarding the event managers target. Previously, I was attaching to the main application dispatch event meaning that the listener would be called on every dispatch event.
I now specifically target the controllers I want to listen to.
$eventManager->attach(array(
'JobboardUser\Controller\AccountController',
'JobboardUser\Controller\ProfileController'
),
MvcEvent::EVENT_DISPATCH,
array($this, 'addUserAccountLayout'),
-80
);
The final listener code looks like this
public function addUserAccountLayout(EventInterface $event)
{
$result = $event->getResult();
if (! $result instanceof ViewModel || $result instanceof JsonModel) {
return;
}
$application = $event->getApplication();
$serviceManager = $application->getServiceManager();
$accountService = $serviceManager->get('JobboardUser\Service\AccountService');
$currentUser = $accountService->getCurrentUser();
$layout = new ViewModel();
$layout->setVariables(array('user' => $currentUser));
$layout->setTemplate('jobboard-user/widget/user-account');
$layout->addChild($result, 'userContent');
$event->getViewModel()->addChild($layout, 'content');
}

Categories