Michaël Gallego bio photo

Michaël Gallego

This is my blog. What can you expect here? Well... Zend Framework 2, Amazon AWS...

Twitter

Google+

LinkedIn

Github

Last.fm

How to replace the "Action" helper in ZF 2 (and make great widgetized content)

I had a great discussion yesterday with Robert Basic about the missing “Action” view helper in Zend Framework 2. As many people ask about it, I decided to blog about it. I was asking him about it because ZfcTwig (the official ZF 2 module to Twig, an awesome template engine by Fabien Potencier, try it out, it makes your views sexy) has a built-in “Action” helper, but I had the feeling something was wrong about that.

Pour la version française de cet article, c’est ici ! For the Russian version, click here (thanks Oleg Lobach) !

How it worked in ZF 1

Back in ZF 1, the action helper was mainly used for “widgetized” content, and allowed to start a new dispatch process to another action within the view. It worked like this :

<?php echo $this->action('list', 'comment', null, array('count' => 10)); ?>

This code just calls the “list” action of the “CommentController” with some parameters, and returns the HTML code from it. Nice and easy, isn’t it ?

However, this suffers from a lot of flaws. The first of them is performance. Because this begins a new MVC process is began (dispatch, routing…). Of course, all of the hooks you may have added through events (ACL…) are executed again, too. This may lead to side-effects that are very hard to debug, and the performance can be really bad, too.

The second reason is that it introduces a dependency from your view to the controler. Basically, the following schema is done when using “action” view helper :

controler -> view -> controler -> view.

Thirdly, when dealing with widgetized content, you need to have action (and hence, routes) for every action you may call in your “action” view helper. This means that you have to creata a lot of meaningless actions (because they are never called alone) and a lot of useless routes.

In a nutshell, this is bad (the rumor said that Matthew Weier O’Phinney really considers it as evil ;-)). As a consequence of this, the Action view helper was completely removed from ZF 2.

The solution

Well, of course, there are solutions (once again, thanks to Robert Basic for giving me one of them !).

The Forward solution

The first solution is using the Forward controller plugin. The Forward plugin allows you, inside an action, to dispatch to another action, but without all the overhead (no routing is done). For instance, let’s say that, inside a specific page, you want to display an invoice. Because the invoice could be rendered alone, and that it makes sense to delegate this task to another controller, you could do this:

public function invoiceDetailsAction()
{
    $mainViewModel = new ViewModel();

    [...] // Do work, set variables for this page...

    // And get the widget for the invoice
    $invoiceId         = $this->params('id');
    $invoiceWidget = $this->forward()->dispatch('Application\Controller\Invoice', array(
        'action' => 'display',
        'id'     => $invoiceId
    ));

    return $mainViewModel->addChild($invoiceWidget, 'invoiceWidget');
}

This action first create a ViewModel instance for this page (invoice-details action). You can fetch and send to the view some details that are specific to this page.

Then, I call the forward controller plugin :

$invoiceWidget = $this->forward()->dispatch('Application\Controller\Invoice', array(
    'action' => 'display',
    'id'     => $invoiceId
));

The first parameter is the Controller name, and the second parameter is an array of params. Most of the time, you will set the ‘action’ key (as a consequence of this, you do not need to explicitely add a route, has the forward plugin already has everything he needs to dispatch the request).

The Forward() plugin returns a ViewModel. Finally, we add this view model as a child of the main view model. In your view, you just need to do that:

<?php echo $this->invoiceWidget; ?>

Nice and clean. Your code is well separated, it does not suffer from performance problems, and does not introduce any reference to the controller within the view.

The View Helper method

But this method suffers from a big flaw : this is perfect when your widgetized content is drawn in one, eventually two, pages.

But what if you want to draw a “Meteo” widget in all your pages ? Of course, you don’t want to “forward” in EVERY actions. This would be error prone, and make your code a lot less maintenable. For this specific case, the action helper was seen as perfect, as you would do this in your view:

<?php echo $this->action('displayWidget', 'Meteo', null, array('city' => 'Paris')); ?>

Hopefully, there is a much better alternative in Zend Framework 2 : view helpers. The idea is to create a view helper for every “widget”. Your view helper will fetch data from either database (in this case, you have to handle the dependencies through the ServiceManager, as we will see later), or a web service, for instance. Once it has the data, it will call the “Partial” helper (or manually render the HTML directly in the view helper), and return the generated HTML.

Let’s stay with our Meteo example by creating a simple View helper for that. Our view helper will have a dependency to a “MeteoService”:

namespace Application\View\Helper;

use Application\Service\MeteoService;
use Zend\View\Helper\AbstractHelper;

class MeteoWidget extends AbstractHelper
{
    protected $meteoService = null;

    public function __construct(MeteoService $meteoService)
    {
    	$this->meteoService = $meteoService;
    }

    public function __invoke($city)
    {
        $temperature = $this->service->getTemperature($city);

        return $this->getView()->render('application/meteo/display', array('temperature' => $temperature));

        // If a full template is overkill, you could of course just render
        // the widget directly
        return ">div>The temperature is $temperature degrees>/div>";
    }
}

Because the View Helper has a dependency, you will have to tell the Service Manager how to handle those dependencies:

// In your Module.php

public function getViewHelperConfig()
{
	return array(
		'factories' => array(
			'meteoWidget' => function ($serviceManager) {
				// Get the Meteo Service
				$meteoService = $serviceManager->getServiceLocator->get('MeteoService');
				return new \Application\View\Helper\MeteoWidget($meteoService);
			}
		)
	);
}

And in your views:

<?php echo $this->meteoWidget('Paris'); ?>

And voilà ! You have a nice widgetized architecture that you can reuse in all of your pages !