Michaël Gallego

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

Twitter

Google+

LinkedIn

Github

Last.fm

Comment remplacer l'aide de vue "Action" dans ZF 2 (et faire du contenu par widget)

J’ai pu avoir une discussion très intéressante hier avec Robert Basic concernant l’absence de l’aide de vue “Action” dans Zend Framework 2. ZfcTwig (le module officiel de Twig, le moteur de template) dispose d’une implémentation de cette aide d’action, mais je me souvenais qu’elle était plutôt décriée dans Zend Framework 1. Heureusement, il existe des alternatives bien meilleures !

For the English version of this article, click here. L’article a égaelement été traduit en russe, par ici.

Dans ZF 1

Dans Zend Framework 1, cette aide de vue était principalement utilisée pour créer du contenu par “widget” en relançant, directement depuis la vue, le processus MVC de zéro (routage, dispatch de la requête, appel à la vue…). Ce qui donnait ceci :

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

Ce code appelait l’action “list” du contrôleur “CommentController”, éventuellement en lui passant certains paramètres, et retournait le résultat (du code HTML). Plutôt simple à gérer et élégant “d’apparence”.

Hélas, cette méthode souffrait de pas mal de soucis (pour en savoir plus). Le premier et le plus pénalisant était d’un point de vue performance. De plus, tous les éventuels hooks qui pouvaient être ajoutés (par preDispatch ou postDispatch par exemple) étaient exécutés autant de fois que cette aide d’action était appelée, ce qui provoquait évidemment des effets de bords difficiles à débugger.

La seconde raison c’est qu’elle introduit une dépendance entre la vue et le contrôleur.

Enfin (et c’est surtout vrai sous ZF 2 ou la bonne pratique encourage à définir explicitement toutes les routes), une aide de vue Action oblige à définir des routes pour ces actions qui, par définition, ne seront jamais affichées “seules”.

Bref, en un mot comme en cent, il fallait fuire comme la peste l’aide de vue Action sous ZF 1. C’est pour ces raisons qu’elle n’a pas été introduite dans ZF 2.

Les solutions

Evidemment, il existe des alternatives bien plus avantageuses. En voici deux :

La solution avec le plugin de contrôleur Forward

La première solution consiste à utiliser le plugin de contrôleur “Forward”. Ce plugin permet, à l’intérieur d’une action, de dispatcher vers une autre action (éventuellement d’un autre contrôleur), sans souffrir de tous les désavantages d’un point de vue performance (puisqu’il n’y a pas de routage à faire). Par exemple, dans une de mes applications, j’ai besoin d’afficher une facture. Parce que cette facture pourrait être affichée “seule” (dans un PDF…), et parce que cela a du sens de déléguer cette tâche à un autre contrôleur (j’aime bien l’idée que chaque action de chaque contrôleur ait ses propres tâches à faire et qu’elles n’empiètent pas trop), nous pourrions faire ceci :

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

    [...] // Faire quelque chose...

    // Récupérer le widget pour la facture
    $invoiceId         = $this->params('id');
    $invoiceWidget = $this->forward()->dispatch('Application\Controller\Invoice', array(
        'action' => 'display',
        'id'     => $invoiceId
    ));

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

L’action créé d’abord une instance de la classe “ViewModel” pour cette page (l’action invoice-details). Evidemment, vous pouvez récupérer et envoyer à la vue des informations.

Puis j’effectue l’appel à “Forward” :

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

Ce plugin de contrôleur prend en premier paramètre le nom du contrôleur, et en second paramètre un tableau contenant des paramètres. La plupart du temps, on définit explictement l’action vers laquelle dispatcher grâce à la clé “action” (n’oubliez pas que si votrre action s’appelle “displayInvoiceAction”, la valeur a donner pour la clé “action” sera “display-invoice” et non “displayInvoice” ou “displayInvoiceAction”).

Le plugin Forward() retourne lui même une instance d’un ViewModel. On l’ajoute donc comme ViewModel enfant du ViewModel principal, et on lui donne un nom (ici “invoiceWidget”) afin de pouvoir l’afficher dans la vue, comme ceci :

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

Cette solution est simple à mettre en oeuvre et plutôt élégante. Elle ne souffre d’aucun des problèmes de l’aide de vue Action, et permet de garder une bonne séparation de la logique de votre code.

La méthode avec les aides de vue

Evidemment, le problème de la méthode précédente est évidente : c’est parfait si vous devez afficher votre widget dans une voir deux pages.

Mais si vous souhaitez afficher un widget “Meteo” dans toutes vos pages, il est évidemment inconcevable de faire un “forward” dans TOUTES vos actions. Sans compter que si vous y trouvez un bug, ou que vous souhaitez tout simplement supprimer ce widget de toutes vos pages, vous risquiez d’y passer quelques heures… ;-). A cet égard, l’aide de vue “Action” de ZF 1 présentait la solution “intuitive” à ce problème :

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

Heureusement, il existe une bien meilleure alternative sous Zend Framework 2 : les aides de vue. L’idée consiste à créer une aide de vue pour chaque “widget”. Par exemple, votre aide de vue va récupérer des données à partir d’un web service ou de la base de données (dans ce cas, une dépendance avec un Service ou un DataMapper sera sans nul doute introduite, comme nous allons le voir). Une fois les données récupérées, cette aide de vue va appeler l’aide de vue Partial ou PartialLoop pour l’aider à rendre ces données dans un template (ou, si le widget est petit, directement générer le code HTML manuellement), puis retourner le résultat afin qu’il soit afficher dans la vue principale.

Reprenons notre example de la Meteo, et voici un exemple d’aide de vue (ici, j’ai introduit une dépendance avec un “MeteoService” qui va récupérer les informations dont on a besoin.

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));

        // Vous pouvez aussi rendre le HTML manuellement :
        return "<div>La température est de : $temperature degrés</div>"
    }
}

Il ne reste plus qu’à indiquer au ServiceManager comment trouver l’aide de vue, et également définir la dépendance :

// Dans Module.php

public function getViewHelperConfig()
{
	return array(
		'factories' => array(
			'meteoWidget' => function ($serviceManager) {
				// Récupérer le service Meteo
				$meteoService = $serviceManager->getServiceLocator->get('MeteoService');
				return new \Application\View\Helper\MeteoWidget($meteoService);
			}
		)
	);
}

Et dans votre vue :

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

Et voilà !