Pour la version française de cet article, c’est ici !
For the Russian version, click here (thanks Oleg Lobach) !
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.
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 :
|
1 |
<?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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 :
|
1 2 3 4 |
$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 :
|
1 |
<?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 :
|
1 |
<?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” :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 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 :
|
1 |
<?php echo $this->meteoWidget('Paris'); ?> |
And voilà ! You have a nice widgetized architecture that you can reuse in all of your pages !
Ping : Comment remplacer l’aide de vue “Action” dans ZF 2 (et faire du contenu par widget) | Un blog sur tout et rien…
Please show Application\Service\MeteoService.php contents
Hi,
This is out of scope of this article and I didn’t write such a service. Basically, the service will be the one that contains the db mapper and return results from databases. Services are also used in controllers.
I suggest you to have more information about this.
Hi Michaël!
I don’t understand that in the class of MeteoWidget __invoke method, how call the $this->service->getTemperature($city);
Because I can’t see refer code in this.
Thanks your help
Hi,
As I told you the goal of this post was not to explain services. I’ve done this code just for example so I didn’t written MeteoService code, this was just an example.
If its clearer for you, just imagine the MeteoService as a class that will call a web service to get the temperature. In other cases, it can be used to retrieve an element from database. Once again, this example is not complete it was just to demonstrate how view helpers can help to create widgetized content
.
I understand Thanks your help Michaël
Hi Michaël!
Very nice post! Thanks!
If you don’t mind, I want to translate it to Russian and post on my blog.
Sure
.
Please send me the link once it is translated so I can add it here
.
Ok. Deal
Done. Here you go: http://zftutorials.ru/blog/how-to-replace-action-helper-in-zf2.html
Ping : Zend Framework Tutorials / Чем заменить хелпер «Action» в ZF2
Hello. Could you so kind to see my question on Stackoverflow? http://stackoverflow.com/questions/13766039/zf2-how-to-pass-parameters-to-forward-plugin-which-i-can-then-get-in-the-method
I can’t get how to pass parameters with the forward method.
Thank you.
Their is a hole in teh approach while adding a child in the viewModel. If the action of the child view model also returns a view model. Than this approach generated an error :/
I used viewHelper solution in zf1. ViewHelpers are lightweigt, and this is nice. Second step was, wrtie my own class with additional functionality, separete view, some usefule metods like getParam:), init, cache, and i realized that my view helper became small ActionController:) Most of all code was placed in view helpers not in controllers. Controllers was only for routing purpose. It works, its simple, its fast, but is it correct aproach?
Out of interest I am trying to get the $this->forward()->dispatch() method to work but using ZfcTwig. The $view->addChild() works if the view is a php view, but when I change it to a twig view is does not work. Have you tried it with a twig view. Not sure how to event trouble shoot or check it.