Factoriser les actions redondantes
Au fil des projets CakePHP, nous nous apercevons très vite qu’un certain nombre d’actions de nos Contrôleurs sont répétées quasiment à l’identique, en particulier les actions d’administration. Dans la mesure où tous les Contrôleurs de l’application héritent de la classe AppController, profitons-en pour factoriser les actions redondantes sous la forme la plus générique possible.
La classe AppController
Commençons donc par écrire les actions d’administration classiques qui vont nous servir dans quasiment tous les cas. Elles sont au nombre de 5 :admin_index: simple pagination des enregistrements ;admin_view: vue d’un enregistrement ;admin_add: ajout d’un enregistrement ;admin_edit: édition d’un enregistrement ;admin_delete: suppression d’un enregistrement ;admin_invert_field: inverser la valeur d’un champ, défini dans la base comme un TINYINT et de longueur 1, qui sera géré par Cake comme s’il s’agissait d’un booléen. Très utile pour gérer un attribut de validité, de visibilité, de publication, etc. directement depuis la vue de l’actionadmin_index.
<?php // {app}/app_controller.php class AppController extends Controller { /** * Méthode générique de pagination des enregistrements * */ function admin_index() { $this->set('data', $this->paginate()); } /** * Méthode générique de consultation d'un energistrement * * @param int $id Id de l'enregistrement */ function admin_view($id = null) { if (!$data = $this->{$this->modelClass}->read(null, $id)) { $this->Session->setFlash("Enregistrement introuvable.", 'message_error'); $this->redirect(array('action' => 'index')); } $this->set(compact('data')); } /** * Méthode générique d'ajout d'un enregistrement */ function admin_add() { $model =& $this->{$this->modelClass}; if (!empty($this->data)) { $model->create(); if (!$model->save($this->data)) { $this->Session->setFlash("Veuillez corriger les erreurs.", 'message_notice'); return; } else { $this->Session->setFlash("Enregistrement créé.", 'message_ok'); $this->redirect($this->Session->read('Temp.referer')); } } $this->Session->write('Temp.referer', $this->referer()); } /** * Méthode générique d'édition d'un enregistrement * * @param int $id Id de l'enregistrement à modifier */ function admin_edit($id = null) { $model =& $this->{$this->modelClass}; if (!empty($this->data)) { $model->set($this->data); if (!$model->save()) { $this->Session->setFlash("Veuillez corriger les erreurs.", 'message_notice'); return; } else { $this->Session->setFlash("Enregistrement mis à jour.", 'message_ok'); $this->redirect($this->Session->read('Temp.referer')); } } $this->data = $model->read(null, $id); $this->Session->write('Temp.referer', $this->referer()); } /** * Méthode générique de suppression d'un enregistrement * * @param int $id Id de l'enregistrement à supprimer */ function admin_delete($id = null) { if (!$this->{$this->modelClass}->delete($id)) { $this->Session->setFlash("Enregistrement introuvable.", 'message_error'); } else { $this->Session->setFlash("Enregistrement supprimé.", 'message_ok'); } $this->redirect($this->referer()); } /** * Inverser la valeur d'un champ numérique * * @param int $id Id de l'enregistrement concerné * @param string $field Nom du champ dont on veut inverser la valeur */ function admin_invert_field($id = null, $field = 'disabled') { $model =& $this->{$this->modelClass}; if (!$model->hasField($field)) { trigger_error(sprintf('%s::%s(): champ absent %s.%s', __class__, __FUNCTION__, $this->modelClass, $field), E_USER_ERROR); } else { $model->updateAll( array($model->alias.'.'.$field => "1 - `{$model->alias}`.`{$field}`"), array($model->alias.'.'.$model->primaryKey => $id) ); $this->Session->setFlash("Enregistrement mis à jour.", 'message_ok'); } $this->redirect($this->referer()); } } ?>
Ces 5 actions peuvent suffire à gérer une administration complète de toutes nos entités !
Redéfinitions éventuelles
Il reste cependant évident que certains Contrôleurs nécessiteront un traitement particulier de certaines actions. Gardons toujours à l’esprit la souplesse apportée par l’existence des callbacks de Modèle comme beforeSave, afterSave, etc, qui permettront déjà de gérer la plupart des exceptions. Si vraiment une action demande une gestion totalement différente de l’action générique de l’AppController, il nous suffit de la redéfinir directement dans le Contrôleur concerné. Imaginons par exemple un Contrôleur qui doit enregistrer l’id de l’administrateur logué avant la sauvegarde d’un enregistrement :
<?php // {app}/controllers/exemples_controller.php class ExemplesController extends AppController { // Pas besoin de redéfinir la méthode admin_index, la variable $paginate suffit : var $paginate = array( 'order' => 'Exemple.created DESC', 'limit' => 10 ); function admin_add() { if (!empty($this->data)) { $this->data['Exemple']['user_id'] = $this->Auth->user('id'); } parent::admin_add(); } } ?>
Nous complétons le tableau des données venant du formulaire, $this->data, avec l’id de l’utilisateur logué, et nous faisons appel à la méthode générique de l’AppController qui va prendre le relais.
Toutes les autres actions d’administration, même si elles ne figurent pas dans le Contrôleur Exemples, seront bien appelées quand nous nous rendrons à l’URL /admin/exemples/index (ou edit/{$id}, ou delete/{$id}).
Commentaires
29 janvier 2009 à 23:49
Tout simplement génial ! Cette idée me trottait en tête depuis un bon moment … mais je n’ai jamais pris le temps d’explorer plus loin pour trouver le coup du « modelClass ».
Bravo !
12 mars 2009 à 2:50
Bonsoir,
J’ai remarqué un élément particulièrement étrange concernant la classe générique admin_edit().
Il se trouve que tel que tel que tu la écrite, la fonction ne permet pas de remplir les champs du formulaire avec les données de la BDD. C’est incompréhensible, puisque tout est passé par référence, et que tout le reste (enregistrement, vérification) fonctionne.
J’ai du supprimer la déclaration :
$this->data = $model->read(null, $id);de l’AppControlleur.Mon adminedit du controlleur courant est donc :
function adminedit($id = null) { parent::admin_edit(); $model = $this->LeNomDeMonModele; //& $this->{$this->modelClass}; $this->data = $model->read(null, $id); }C’est le seul moyen que j’ai trouvé pour que les champs du formulaire soient remplis avec les données de l’enregistrement.
As tu une explication ?
12 mars 2009 à 4:05
D’ailleurs, pour la fonction admin_delete, on se retrouve également avec une erreur : missing view. Il y a donc une variable (?) qui est mal transmise, puisque aucune vue n’est nécessaire pour delete. Est – ce que ce ne pourrait pas être l’id qui n’est pas transmit correctement ?
12 mars 2009 à 8:51
Sur quelle version de CakePHP travailles-tu ?
12 mars 2009 à 9:32
Bonjour, Sur la dernière stable à ce jour : 1.2.1.8004
12 mars 2009 à 14:23
j’avance un peu : Ce n’est pas un problème de transmission d’id, c’est $this->data qui est vide. $model->read(null, $id) ne retourne rien.
$model =& $this->{$this->modelClass}; if ($model){ pr( 'Le modele existe et s\'appelle : '.$this->modelClass ); } $this->data = $model->read(null, $id); pr($this->data); // vide si dans AppControllerPlacé dans le controlleur courant ou dans l’app controlleur, on a bien « le modele existe t s’appelle ModeleCourant. Cette donnée existe bien.
En revanche, pr($this->data) retourne le tableau des valeurs si placé dans le controlleur courant, alors qu’il ne retourne rien si dans l’app controlleur.
Et là, je sèche.
12 mars 2009 à 15:30
J’ai compris, tout est ma faute :
Les fonctions nécessitant un $id ne doivent en aucun cas être redéclarées « vides » dans le controlleur courant :
function admin_edit($id = null){ }Si une redéclaration doit être faite, il faut obligatoirement passer l’$id :
function adminedit($id = null){ parent::adminedit($id); }Ce qui en soit est on ne peut plus logique :s
Je crois que tu peux supprimer mes interventions précédentes, qui risqueraient d’embrouiller tout autre lecteur.
12 mars 2009 à 15:37
Je pense que je vais les laisser au contraire, ça peut être utile à qui ferait la même erreur !
12 mars 2009 à 16:27
C’est une idée
)
20 avril 2010 à 15:32
J’ai eu un souci avec la fonction invert_field()
en faite le problème venait de
$model->field($field) me revois la valeur du champ donc soit 1 ou 0 et pas un booléen. donc cela posait problème de mettre ! devant.
je suis passé par une condition ternaire a la place
10 août 2010 à 21:23
Superbe ! Bravo