Revenir
en haut

Les aides d’action (helpers), factorisation, avoir le réflexe

22/10/2010 0

S’il y a une chose agaçante en programmation, c’est bien de répéter des dizaines de fois le même morceau de code, la même condition dans chaque méthode, qui ne le fait pas ?

Le modèle de conception MVC impose une certaine granularité des différentes « actions » assurées par l’application. Chaque contrôleur possèdent un certain nombre d’actions possédant chacune un rôle bien défini (le plus souvent un cas d’utilisation).

Plus le nombre d’actions se multiplie, plus la redondance de code devient inévitable.

Le premier réflexe pour un bon développeur orienté objets qui se respecte, sera de créer des super-classes de contrôleur et d’user de l’héritage pour factoriser le code. Mais l’héritage ne répond pas à tous les problèmes.

Ce n’est parfois pas très flexible dans le sens où chaque contrôleur n’a pas forcément besoin des mêmes fonctionnalités, alors que tous possèdent un pan commun. Dans ce cas là, vous vous retrouvez à créer plusieurs super-classes pour chaque groupe de fonctionnalités mais dans lesquelles vous répétez finalement du code commun à chaque super-classe.

Que faire alors ? Créer encore une super-classe et se retrouver avec un nivelage d’héritage important qui complexifie la compréhension du code et devient difficile à maintenir ? Non :)

Les aides d’action ou helpers, mangez-en !

Les aides d’action répondent exactement à cette problématique. Ce sont des plugins (ne pas confondre avec les plugins de contrôleurs !) qui permettent d’ajouter des fonctionnalités aux contrôleurs d’actions sans avoir à étendre de classe. Ils peuvent soit être injectés automatiquement, soit à la demande du développeur, dixit la documentation du ZF : Aides d’actions (Helper).

Mais lorsqu’on débute dans l’utilisation d’un framework tel que Zend Framework, l’utilisation de tels mécanismes n’est pas innée, loin de là. Et pourtant, le ZF regorge de fonctionnalités variées qui facilitent le développement et l’organisation du code, ce serait dommage de s’en priver. Je vous encourage donc à vous forcer à utiliser les aides d’actions jusqu’à ce que ça devienne un réflexe.

Automatiser l’exécution d’un helper (hooks)

Les aides d’actions peuvent soit être invoquées à la demande, soit intervenir automatiquement à des moments clés du processus de dispatch des contrôleurs d’action.

Pour ça, il existe trois méthodes (hooks) :

  • init() : invoquée automatiquement au moment de l’initialisation du contrôleur d’actions.
  • preDispatch() : invoquée juste avant la routine preDispatch() du contrôleur d’actions.
  • postDispath() : invoquée juste après le postDispatch() du contrôleur.

Ces trois méthodes sont invoquées automatiquement pour chaque aide d’action enregistrée, il est donc important de bien choisir à quel moment un helper doit intervenir dans le processus.

Exemple (simple): Restreindre les actions à un verbe HTTP

Le champ d’application des aides d’action est très varié, il peut s’agir aussi bien d’une fonctionnalité basique ne nécessitant que quelques lignes de code que de l’implémentation d’une logique complexe. Je ne vais donc pas m’enfoncer dans un exemple trop compliqué et plutôt vous montrer un cas typique d’utilisation qui peut vous faciliter la vie, à vous et votre imagination de faire le reste :)

Qui n’a jamais utilisé cette ligne de code dans une action de contrôleur ?

if (!$this->getRequest()->isPost()) {
    throw new Zend_Controller_Action_Exception('Bad request method', 405);          
}

Dans de gros projets, c’est typiquement un morceau de code qui sera répété des dizaines de fois, parfois plusieurs fois dans le même contrôleur. Pourquoi ne pas utiliser une aide d’action similaire au context switcher qui permettrait de déclarer un simple attribut dans le contrôleur, exemple :

public $httpContexts = array(
    'mon-action' => array('post'),
    'autre-action' => array('get')
);

Voilà le code du helper, dans lequel vous remarquerez, que fainéant comme je suis, j’ai copié/collé pas mal de code d’un helper déjà existant dans le ZF :)

<?php
/**
 * Simplify http verb verification
 *
 * @uses       Zend_Controller_Action_Helper_Abstract
 * @category   Tight
 * @package    Tight_Controller
 */

class Tight_Controller_Action_Helper_HttpContext extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * Supported contexts
     * @var array
     */

    protected $_httpContexts = array();
   
    /**
     * Controller property key to utilize for context
     * @var string
     */

    protected $_httpContextKey = 'httpContexts';
   
   
    /**
     * Initialize context detection
     *
     * @throws Zend_Controller_Action_Exception
     * @return void
     */

    public function preDispatch()
    {
        $controller = $this->getActionController();
        $request    = $this->getRequest();
        $action     = $request->getActionName();

        // Return if no context are defined
        $httpContexts = $this->getActionHttpContexts($action);
        if (empty($httpContexts)) {
            return;
        }
       
        $currentContext = $request->getMethod();
       
        // Check if http context allowed by action controller
        if (!$this->hasActionHttpContext($action, $currentContext)) {
            throw new Zend_Controller_Action_Exception('Bad request method', 405);
        }
    }
   
   
    /**
     * Does a particular controller action have the given context(s)?
     *
     * @param  string       $action
     * @param  string|array $context
     * @throws Zend_Controller_Action_Exception
     * @return boolean
     */

    public function hasActionHttpContext($action, $httpContext)
    {
        $controller = $this->getActionController();
        $action     = (string) $action;
        $httpContextKey = $this->_httpContextKey;

        $allContexts = $controller->{$httpContextKey};

        if (!isset($allContexts[$action])) {
            return true;
        }

        $contexts = $allContexts[$action];

        if (in_array(mb_strtolower($httpContext), $contexts)) {
            return true;
        }

        return false;
    }
   
   
    /**
     * Get contexts for a given action or all actions in the controller
     *
     * @param  string $action
     * @return array
     */

    public function getActionHttpContexts($action = null)
    {
        $controller = $this->getActionController();
        if (null === $controller) {
            return array();
        }
        $action     = (string) $action;
        $contextKey = $this->_httpContextKey;

        if (!isset($controller->$contextKey)) {
            return array();
        }

        if (null !== $action) {
            if (isset($controller->{$contextKey}[$action])) {
                return $controller->{$contextKey}[$action];
            } else {
                return array();
            }
        }

        return $controller->$contextKey;
    }
}

Ici j’utilise le « hook » preDispatch() qui sera systématiquement invoqué pour chaque contrôleur d’action juste avant de dispatcher l’action. Attention de bien enregistrer l’aide dans le broker d’actions dans ce cas là.

Dans le bootstrap (par exemple) :

protected function _initHelpers()
{
    Zend_Controller_Action_HelperBroker::addHelper(new Tight_Controller_Action_Helper_HttpContext());
}

Avec ça, vous n’avez plus qu’à déclarer votre propriété $httpContexts dans le contrôleur et le tour est joué.

C’est un exemple tout bête, mais qui donne une bonne idée de l’utilité du mécanisme.

Pattern Strategy

Les aides d’actions dans le Zend Framework implémentent le pattern Strategy sur le helper broker (gestionnaire d’aides d’action) qui permet ici d’invoquer directement une aide d’action comme s’il s’agissait d’une méthode du broker.

Il s’agit en fait d’utiliser la méthode clé direct() comme un proxy vers une méthode du helper. Ce mécanisme est utile dans le cas des aides d’action invoquées à la demande et permet de simplifier le code. Exemple avec l’aide d’action « redirector » inclue dans le ZF :

$this->_helper->redirector('mon-action', 'mon-controller', 'mon-module');

// équivalent à
$this->_helper->getHelper('Redirector')->gotoSimple('mon-action', 'mon-controller', 'mon-module');

Si nous jettons un coup d’oeil à la source du helper :

    /**
     * direct(): Perform helper when called as
     * $this->_helper->redirector($action, $controller, $module, $params)
     *
     * @param  string $action
     * @param  string $controller
     * @param  string $module
     * @param  array  $params
     * @return void
     */

    public function direct($action, $controller = null, $module = null, array $params = array())
    {
        $this->gotoSimple($action, $controller, $module, $params);
    }

D’autres exemples

Voici quelques exemples courant d’aides d’action :

  • Automatiser le contrôle des ACLJulien Pauli
  • Historique de navigation (session)
  • Vérifier qu’un utilisateur est authentifié
  • j’ai rien d’autre qui me vient, mais tout ce vous sera utile et qui justifie l’utilisation d’un helper.

Et bien entendu tous les helpers inclus dans le framework que les développeurs oublient souvent d’utiliser :)

Limitations & Conclusion

Comme toujours, un composant ne vient pas sans son lot de critiques que ce soit dans le design, l’implémentation du concept ou son utilisation.

Pour ma part, je trouve l’outil très utile et je serais idiot de m’en passer pour des considérations purement philosophiques. Le seul point, et pas des moindre, qui me limite dans son utilisation, sera la difficulté pour y injecter quoi que ce soit. Le chargement étant assuré par un gestionnaire (le broker) utilisé de manière statique, il est très difficile de pratiquer l’injection de dépendances dans les helpers. Le ZF n’est pas prévu pour fonctionner avec un conteneur de dépendances, ou du moins n’est pas toujours adapté pour ça.

Néanmoins, les aides d’actions restent utiles tant qu’elles ne nécessitent pas de dépendances. Je pense que les développeurs du ZF sont conscients de cette problématique et nous proposerons une implémentation plus flexible dans la version 2 du framework.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

*

Tags HTML autorisés : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Tag code : [cc lang="langage"][/cc] (ex. [cc lang="php"][/cc])