Zend Framework : contrôleur CRUD

Logo Zend Framework

Dans une application MVC qui a besoin de faire persister des données, les contrôleurs doivent réaliser 4 opérations de base qu'on désigne souvent par l'abréviation CRUD : Create, Read, Update et Delete. Pour une application développée avec le Zend Framework, un contrôleur est en fait une classe dérivée de Zend_Controller_Action. Afin d'éviter de dupliquer inutilement du code, les développeurs choisissent souvent d'écrire une classe de base qui fournira une implémentation standard de ces opérations.

Un contrôleur remplit énormément de fonctions et est très intimement lié à l'application. Construire un contrôleur CRUD générique qui pourra être réutilisé entre plusieurs applications n'est pas une chose simple et risque d'engendrer un monstre très compliqué et bourré d'options de configuration. De plus, chaque développeur a ses préférences et chaque application ses exigences. C'est pourquoi au lieu de vous proposer une classe toute faite, je vais développer dans plusieurs articles car le sujet est très vaste, les différentes étapes pour développer une classe CRUD en proposant à chaque fois des options que vous pourrez choisir d'ajouter ou non à vos contrôleurs.

Squelette de la classe

Les 4 opérations CRUD vont se traduire dans notre contrôleur par 4 actions, donc 4 méthodes dont le nom se terminera par Action. Souvent l'opération Read se décomposera en deux opérations : index qui liste les éléments de la collection et view qui affiche une vue détaillée d'un des éléments. Pour les opérations create et update, je préfère personnellement utiliser les termes add et edit. Le squelette de notre classe ressemblera donc à ceci :

<?php
abstract class Wiip_Controller_Action_CRUD extends Zend_Controller_Action
{
    public function 
addAction()
    {
    }

    public function 
deleteAction()
    {
    }

    public function 
editAction()
    {
    }

    public function 
indexAction()
    {
    }

    public function 
viewAction()
    {
    }
}
?>

On déclare la classe comme abstraite puisqu'on ne va jamais l'instancier directement mais s'en servir comme base pour nos contrôleurs.

Déduire le nom du modèle d'après le nom du contrôleur

Pour accéder au modèle, on a besoin de connaitre le nom de la classe qui lui est associée. Afin éviter d'avoir à le spécifier à chaque nouveau contrôleur, on va suivre une convention de nommage qui va permettre de déduire le nom du modèle d'après le nom du contrôleur.

La convention de nommage utilisée pour le nom des contrôleurs suit la convention UpperCamelCase. Le nom de la classe commence donc par une majuscule ainsi que chaque mot composant le nom du contrôleur. On ajoute ensuite le suffixe Controller. Les URL restent elles toujours en minuscules avec chaque mot séparé par un tiret ou par un point. Exemple :

  • /posts-comments/add : URL pour appeler l'action add du contrôleur PostsComments
  • PostsCommentsController : nom de la classe associée au contrôleur

Pour ce qui est du nom du modèle, le guide du démarrage du ZF utilise la convention suivante pour les modèles : Module_Model_Nom_Du_Modele. Si on reprend l'exemple précédent, cela donnera Default_Model_Posts_Comments.

<?php
abstract class Wiip_Controller_Action_CRUD extends Zend_Controller_Action
{    
    
/**
     * Nom de la classe associée à la table
     * Optionnel. Si non spécifié, on le déduit du nom de la classe du contrôleur
     *
     * @var string
     */
    
protected $_tableClass;

    [..]

    
/**
     * Retourne le nom de la classe associée à la table
     *
     * @return string
     */
    
protected function _getTableClass()
    {
        if (!
$this->_tableClass) {
            
// Récupère le nom de la classe du contrôleur
            
$cls get_class($this);
            
// Supprime le suffixe Controller
            
$this->_tableClass substr($cls0strlen($cls) - 10);
            
// Passe de la notation UpperCamelCase à une notation ou les mots sont
            // séparés par des blancs soulignés
            
$this->_tableClass preg_replace('/([a-z])([A-Z])/''$1_$2'$this->_tableClass);
            
// Récupère le nom du module et mets en majuscule la première lettre
            
$module ucfirst($this->_getParam('module'));
            
// Assemble les différents éléments pour obtenir le nom de la classe du modèle
            
$this->_tableClass $module '_Model_' $this->_tableClass;
        }
        return 
$this->_tableClass;
    }

    [..]
}
?>

Instancier le modèle

Rien de plus simple une fois qu'on a le nom de la classe :

<?php
abstract class Wiip_Controller_Action_CRUD extends Zend_Controller_Action

    
/**
     * Instance du modèle
     *
     * @var $_table Wiip_Model_Abstract
     */
    
protected $_table;

    [..]

    
/**
     * Retourne une instance du modèle
     *
     * @return Zend_Db_Table
     */
    
protected function _getTable()
    {
        if (!
$this->_table) {
            
$tableClass $this->_getTableClass();
            
$this->_table = new $tableClass;
        }
        return 
$this->_table;
    }


    [..]
}
?>

A l'usage, on se rend compte qu'on a besoin d'accéder au modèle pratiquement pour chaque requête. Pour éviter d'avoir à appeler systématiquement la méthode _getTable, on peut appeler cette dernière dans le constructeur :

<?php
abstract class Wiip_Controller_Action_CRUD extends Zend_Controller_Action

    [..]

    public function 
__construct(Zend_Controller_Request_Abstract $request
        
Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {        
        
// On crée automatiquement le modèle avant d'appeler le constructeur
        // de Zend_Controller_Action. Comme ça, le modèle sera disponible
        // dans les méthodes init.
        
$this->_getTable();

        
parent::__construct($request$response$invokeArgs);
    }

    [..]
}
?>
Je surcharge le constructeur au lieu d'utiliser la méthode init. De cette façon, on peut surcharger cette dernière sans avoir à appeler la méthode parente dans nos contrôleurs.

Retrouver l'enregistrement

Pour les opérations delete, edit et view, la première étape sera de retrouver l'enregistrement. On peut factoriser le code nécessaire en le plaçant dans une méthode dédiée :

<?php
abstract class Wiip_Controller_Action_CRUD extends Zend_Controller_Action
{
    
/**
     * Enregistrement sur lequel les opérations sont effectuées
     *
     * @var Zend_Db_Table_Row
     */
    
protected $_row;

    [..]

    
/**
     * Retrouve l'enregistrement
     * 
     * @return Zend_Db_Table_Row 
     */    
    
protected function _getRow()
    {
        if (!
$this->_hasParam('id')) {
            throw new 
Zend_Controller_Action_Exception('Pas d\'ID dans les paramètres'500);
        }
        
$this->_row $this->_table->find($this->_getParam('id'))->current();
        if (!
$this->_row) {
            throw new 
Zend_Controller_Action_Exception('Document non trouvé'404);
        }
        return 
$this->_row;
    }

    [..]
}
?>

Si l'ID passé en argument de la requête ne correspond pas à un enregistrement, on affiche une erreur 404 (page non trouvée).

La prochaine fois...

On verra comment implémenter les méthodes delete, add, edit et view et comment générer et traiter les formulaires.

Commentaires

Juste une précision :
Le faite de créer une classe qui étend Zend_Controller_Action et qui sert de base à tout les contrôleur n'est pas très flexible (et peut être pas compatible avec de future version du framework) . Le mieux je pense est de créer une interface du type
XX_Controller_Action_Crud qui impose l'implémentation des méthodes CRUD. Ensuite il suffit de créer des aides d'actions qui seront appelées dans les différents contrôleurs.

Voici ce que précise la doc zend :

Action Helpers allow developers to inject runtime and/or on-demand functionality into any Action Controllers that extend Zend_Controller_Action. Action Helpers aim to minimize the necessity to extend the abstract Action Controller in order to inject common Action Controller functionality.

ou encore :


Many tutorials on Zend Framework would have you believe you should create a base class extending Zend_Controller_Action to provide base functionality for your controllers:

However, this is not only not necessary, but typically not a great move for extensibility. You may find later that a given controller only needs a subset of the methods in your base controller -- or that you're constantly adding methods that only a few of your controllers need, creating bloat.

Sinon bon tuto :)

Je pense qu'il vaut mieux réserver les aides d'action aux fonctions optionnelles comme le support d'Ajax par exemple. C'est vrai qu'elles peuvent apporter une grande flexibilité, mais le CRUD, c'est quand même la fonctionnalité de base du contrôleur et donc je pense que c'est plus simple et plus efficace d'étendre Zend_Controller_Action.

Pour l'interface, ça peut être effectivement intéressant si on a plusieurs types de modèles. On pourrait alors avoir une interface XX_Controller_Action_Crud_Interface et des implémentations XX_Controller_Action_Crud_ZendDbTable ou XX_Controller_Action_Crud_Doctrine par exemple.

Thanks

Merci pour ce tuto :)
A quand la suite ?

Cette semaine, ça va être chaud (retour de vacances). Peut-être la semaine prochaine.

A quand la suite de ce super tuto ;-)

Bonjour,

Une suite est-elle prévu pour cette excellent tuto?

Ajouter un commentaire