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');
}
Related
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');
}
}
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__);
}
I am trying to change the layout based on my routes, here what I have done until now:
<?php namespace Application\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;
use Zend\View\Model\ModelInterface as Model;
class LayoutListener extends AbstractListenerAggregate
{
/**
* Attach one or more listeners
* Implementors may add an optional $priority argument; the EventManager
* implementation will pass this to the aggregate.
* #param EventManagerInterface $events
* #return void
*/
public function attach(EventManagerInterface $events)
{
$sharedEvents = $events->getSharedManager();
$this->listeners[] = $sharedEvents->attach(
'Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,
array($this, 'handleLayouts'),
-99);
}
public function handleLayouts(MvcEvent $event)
{
$viewModel = $event->getViewModel();
if (!$viewModel instanceof Model) {
return;
}
$routeMatch = $event->getRouteMatch();
$resolver = $event->getApplication()
->getServiceManager()
->get('Zend\View\Resolver\TemplatePathStack');
if ('Application\Controller\Index' !== $routeMatch->getParam('controller')) {
return;
}
switch ($routeMatch->getParam('action')) {
case 'index':
$template = 'layout/layout';
break;
case 'home':
$template = 'layout/home';
break;
default:
$template = null;
break;
}
/**
*
* Don't know why, but to set a template must use $event->getViewModel()
* and for terminal must use $event->getResult(), otherwise it won't works
*
*/
// if template is resolvable, use it, otherwise go terminal
if ($template && $resolver->resolve($template)) {
$viewModel->setTemplate($template);
} else {
if ($event->getResult() instanceof Model) {
$event->getResult()->setTerminal(true);
$viewModel->setTerminal(true);
}
}
}
}
Module.php
public function onBootstrap(MvcEvent $e)
{
$translator = $e->getApplication()->getServiceManager()->get('translator');
$translator->setLocale(\Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']))
->setFallbackLocale('en_US');
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attachAggregate(new LayoutListener());
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
There is one thing I don't get, if I want to change the layout, I must use
$event->getViewModel()->setTemplate($template);
otherwise it won't work, and if I want to set the viewModel terminal, I must use
$event->getResult()->setTerminal(true);
otherwise it won't work, can someone explain me why ?
What I'm trying to achieve it's the following: I have one template for index/ that initialize an iFrame loading the content of any other route, so in the index layout I have set up the menu and any other pages don't need to have a layout, as the index/ one is used.
Now I want a subpage having a custom layout (custom display) so, based on the route, I set a new layout to the viewmodel.
You can add child-views to your view-model (if you are in a Controller) - if you make an own controller for your subview (or subpage):
$view = new ViewModel(array(
// parameters
));
$view->addChild($this->forward()->dispatch('SubpageController', array(
'action' => 'index',
)), 'contentOfSubpage');
return $view;
in the template, you can access the contents of the SubpageController like this:
<?php echo $contentOfSubpage; ?>
at least, thats how you can combine multiple controllers, therefore different layouts
$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
}
);
}
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');
});
}