My previous post explains how I’ve set up my module structure, for the modules I create under my ‘Ctrl\Module\’ namespace. That post shows you how to configure the autoloader correctly, so the module classes can be accessed. This post will explain how I configured the view manager to avoid template name collisions that this system could cause.
The problems start when a controller in the Auth module does not return a ViewModel with a template specified. the ZF2 MVC registeres a couple of eventListeners to inject a ViewModel if one was not returned, or inject a template based on the matched module/controller/action pair if a ViewModel was returned, but does not have a template set.
For example the action ‘Ctrl\Module\Auth\Controller\IndexController::indexAction()’ that resides in the Auth module will be injected with the template ‘ctrl/index/index’
As you can see it only uses the root namespace of the module to generate a template name. This may seem fine at first, but what if I add a second module that uses MVC? lets say, a blog module:
The action ‘Ctrl\Module\Blog\Controller\IndexController::indexAction()’ that resides in the Blog module would also be injected with the template ‘ctrl/index/index’
Colliding templates will obviously result in unwanted behavior. There are a couple of things you could do to avoid this problem:
- always return ViewModels with a template specified
- give your modules their own namespace; e.g.: ‘CtrlAuth’ and ‘CtrlBlog’, which would create templates like ‘ctrl-auth/index/index’ by default
- implement you own listener to inject templates
- probably some more stuff…
The best thing for me seemed to be implementing a listener to inject the templates I want, since it gives me the most control, and it’s adaptable for when I decide to use other conventions…
After inspecting the InjectTemplateListener shipped with ZF2, I decided to copy that into my class library and adapt it for my needs:
<?php namespace Ctrl\Mvc\View\Http; use Zend\Mvc\MvcEvent; use Zend\Filter\Word\CamelCaseToDash as CamelCaseToDashFilter; use Zend\Mvc\View\Http\InjectTemplateListener as ZendInjectTemplateListener; class InjectTemplateListener extends ZendInjectTemplateListener { public function injectTemplate(MvcEvent $e) { $routeMatch = $e->getRouteMatch(); $controller = $e->getTarget(); if (is_object($controller)) { $controller = get_class($controller); } if (!$controller) { $controller = $routeMatch->getParam('controller', ''); } //we are only interested in our module's controllers if (strpos($controller, 'Ctrl\\Module') !== 0) { return; } //run the parents logic that will use the $this->deriveModuleNamespace() function //we override below parent::injectTemplate($e); } protected function deriveModuleNamespace($controller) { if (!strstr($controller, '\\')) { return ''; } $parts = explode('\\', $controller); $ns = array(array_shift($parts)); //add root, 'Ctrl' in this case array_shift($parts); //remove 'Ḿodule' namespace $ns[] = array_shift($parts); // add module name return implode('/', $ns); } }
That was easy, just inspect the namespace extract the correct parts to compile the template. This listener would return ‘/ctrl/auth/index/index’ for \Ctrl\Module\Auth\Controller\IndexController::indexAction()
Now all that’s left to do is register the listener so it gets called when we need it. To do this I had a look at how ZF2 registers its default listeners. This is done in the ViewManager class:
<?php namespace Zend\Mvc\View\Http; /** all kinds of stuff here **/ class ViewManager implements ListenerAggregateInterface { /** even more stuff **/ public function onBootstrap($event) { $application = $event->getApplication(); $services = $application->getServiceManager(); $config = $services->get('Config'); $events = $application->getEventManager(); $sharedEvents = $events->getSharedManager(); /** lots of stuff was removed here */ $routeNotFoundStrategy = $this->getRouteNotFoundStrategy(); $createViewModelListener = new CreateViewModelListener(); $injectTemplateListener = new InjectTemplateListener(); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromArray'), -80); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($routeNotFoundStrategy, 'prepareNotFoundViewModel'), -90); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromNull'), -80); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($injectTemplateListener, 'injectTemplate'), -90); } }
Ive copied te relevant parts so we can see what’s going on. Our listener expects a ViewModel to be present, but we want it to be fired before the default template is injected. This puts us right between -90 and -80.
<?php namespace Ctrl; use Zend\ServiceManager\ServiceLocatorInterface; use Ctrl\Mvc\View\Http\InjectTemplateListener; use Zend\Mvc\MvcEvent; class Module { public function onBootstrap($e) { $application = $e->getApplication(); $serviceManager = $application->getServiceManager(); $this->initModules($serviceManager); } protected function initModules(ServiceLocatorInterface $serviceManager) { $eventManager = $serviceManager->get('Application')->getEventManager(); $sharedEvents = $eventManager->getSharedManager(); $injectTemplateListener = new InjectTemplateListener(); $sharedEvents->attach('Ctrl', MvcEvent::EVENT_DISPATCH, array($injectTemplateListener, 'injectTemplate'), -81); } }
easymode