Custom default templates in ZF2

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