Gérer une arborescence avec CakePHP
Pouvoir gérer des données sous forme d’une arborescence est un besoin courant lors de la réalisation d’un site web, nous prendrons ici l’exemple des catégories d’un catalogue de produits. Nous allons nous servir du Comportement Tree fourni par Cake.
1. La table categories
Le Comportement Tree utilise la méthode dite de « représentation intervallaire » pour gérer l’arborescence (en anglais, Modified Preorder Tree Traversal). Nous sortons ici du cadre de CakePHP, et laissons le lecteur découvrir cette méthode de représentation en lisant l’excellent article de SQL Pro : Gestion d’arbres par représentation intervallaire. Retenons simplement que le Comportement Tree impose la présence de 3 champs dans la table de l’arborescence :
parent_id: l’id du parentlft: borne basserght: borne haute
CREATE TABLE `categories` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `parent_id` int(10) UNSIGNED DEFAULT NULL, `lft` int(10) UNSIGNED NOT NULL, `rght` int(10) UNSIGNED NOT NULL, `libelle` varchar(50) collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `parent_id` (`parent_id`), KEY `lft` (`lft`) )
2. Le Modèle Category
Voyons le Modèle associé à notre table :
class Category extends AppModel { var $displayField = 'libelle'; var $hasMany = array('Product'); var $actsAs = array('Tree'); var $validate = array( 'libelle' => array( 'rule' => '/\S+/', 'required' => true, 'allowEmpty' => false, 'message' => "Le libellé de la catégorie doit être renseigné." ), 'parent_id' => array( 'rule' => 'checkParadox', 'on' => 'update', 'message' => "Une catégorie ne peut pas devenir sa propre fille !" ) ); /** * Retourne faux si l'id est égal au nouvel id parent * * @param array $data Données à valider, en provenance du formulaire. * @return boolean Faux si id == parent_id, vrai sinon. */ function checkParadox($data) { if(isset($this->data[$this->alias]['id'])) { return $data['parent_id'] != $this->data[$this->alias]['id']; } return true; } }
Quelques éclaircissements :
- la variable
$displayFieldsera nécessaire pour présenter correctement les catégories sous forme d’une liste déroulante dans un <select> ; - nous définissons une relation
hasManyentre une catégorie et un produit ; - $
actsAs: nous indiquons à Cake que le Modèle Category doit se comporter comme une arborescence ; $validate: deux règles de validation sont mises en place : la première indique que le libellé d’une catégorie ne doit jamais être vide, la deuxième fait appel à une méthode définie juste après qui garantie qu’une catégorie ne peut pas s’avoir elle-même comme parent.
3. Les Vues
Nous n’allons voir que les Vues de l’administration des catégories, à savoir la vue d’ajout/édition, et la vue des catégories sous forme d’un arbre avec la possibilité de monter ou descendre une catégorie dans l’arborescence.
3.1. La Vue d’ajout/édition
<?php // {app}/views/categories/admin_edit.ctp $this->pageTitle = "Ajouter"; if(!empty($this->data['Category']['id'])) { $this->pageTitle = "Modifier"; }; $this->pageTitle .= " une catégorie"; e($form->create('Category', array('action' => 'edit'))); e($form->input('id')); ?> <fieldset> <legend><?php e($this->pageTitle); ?></legend> <?php e($form->input('parent_id', array('label' => "Parent :", 'empty' => "Racine"))); e($form->input('libelle', array('label' => "Nom de la catégorie :"))); ?> </fieldset> <?php e($form->end('Valider')); ?> <p><?php e($html->link("Liste des catégories", 'index')); ?></p>
3.2. La Vue des catégories
Nous allons utiliser pour cette Vue un formidable Helper créé par AD7six (Andy Dawson), que nous récupérons ici : Tree Helper et recopions dans un nouveau fichier {app}/views/helpers/tree.php
<?php // {app}/views/categories/admin_index.ctp $this->pageTitle = "Arborescence des catégories"; echo $tree->generate( $categories, array('element' => 'categories_admin_index') ); ?>
La superbe simplicité de ce Helper réside dans la possibilité d’utiliser un Elément qui sera appelé pour chaque catégorie de l’arborescence, le tout entouré par les balises <ul>…</ul>. Voyons sans attendre ce fameux élément :
<?php // {app}/views/elements/categories_admin_index.ctp $id = $data['Category']['id']; ?> <h4><?php e($data['Category']['libelle']); ?></h4> <ul class="options"> <li><?php e($html->link('Monter', 'move_up/'.$id)); ?></li> <li><?php e($html->link('Descendre', 'move_down/'.$id)); ?></li> <li><?php e($html->link("Modifier", 'edit/'.$id)); ?></li> <li><?php e($html->link("Supprimer", 'delete/'.$id, null, "Etes-vous sûr ?")); ?></li> </ul>
Résultat : moyennant quelques petites règles CSS, nous obtenons une belle arborescence sous forme de listes non-ordonnées imbriquées !
4. Le Contrôleur Categories
Nous terminons par les actions d’administration dans le Contrôleur :
<?php // {app}/controllers/categories_controller.php class CategoriesController extends AppController { // Appel du Helper Tree var $helpers = array ('Tree'); /** * Liste des catégories */ function admin_index() { $this->Category->recursive = -1; $categories = $this->Category->children(false); $this->set(compact('categories')); } /** * Ajout/édition d'une catégorie * * @param int $id Id de la catégorie */ function admin_edit($id = null) { if(isset($this->data)) { $this->Category->set($this->data); if (!$this->Category->validates()) { $this->Session->setFlash("Corrigez les erreurs mentionnées.", 'message_notice'); return; } $this->Category->save(null, false); $this->Session->setFlash("Données enregistrées.", 'message_ok'); $this->redirect('index'); } $this->data = $this->Category->read(null, $id); $this->set('parents', $this->Category->generatetreelist()); } /** * Monte une catégorie d'un cran * * @param int $id Id de la catégorie */ function admin_move_up($id = null) { if(!$this->Category->moveup($id)) { $this->Session->setFlash("La catégorie ne peut pas aller plus haut.", 'message_notice'); } else { $this->Session->setFlash("Ordre mis à jour.", 'message_ok'); } $this->redirect($this->referer()); } /** * Descend une catégorie d'un cran * * @param int $id Id de la catégorie */ function admin_move_down($id = null) { if(!$this->Category->movedown($id)) { $this->Session->setFlash("La catégorie ne peut pas aller plus bas.", 'message_notice'); } else { $this->Session->setFlash("Ordre mis à jour.", 'message_ok'); } $this->redirect($this->referer()); } /** * Suppression d'une catégorie * * @param int $id Id de la catégorie */ function admin_delete($id = null) { $this->Category->id = $id; if(!$this->Category->exists()) { $this->Session->setFlash("Enregistrement introuvable.",'message_error'); } else { $this->Category->removeFromTree($id, true); $this->Session->setFlash("Données supprimées.",'message_ok'); } $this->redirect($this->referer()); } }
Quelques points intéressants :
adminindex: nous faisons appel à la méthodechildren, ajoutée par le Comportement, et qui appelée avec le seul argumentfalserenvoie l’arborescence complète ;adminedit: rappelons que dans la Vue de cette action, nous affichons la liste déroulante des catégories déjà existantes pour pouvoir définir le parent de la catégorie en train d’être créée ou modifiée. Pour obtenir cette liste, nous faisons appel à la méthodegeneratetreelist, ajoutée par le Comportement Tree au Modèle Category. Dans la Vue d’ajout/édition, l’arborescence pour le choix du parent sera sommairement représentée par des underscores _ :- Racine (ajouté dans la définition du <select>)
- Catégorie 1
- Catégorie 1.1
- Catégorie 1.2
- __Catégorie 1.2.1
$this->set('parents',...), il n’est même pas besoin de le dire dans la définition du <select>, la méthode$form->input('parentid')s’en charge !adminmoveupetadminmovedown: ces deux petites actions font appel là encore à deux méthodes ajoutées automatiquement par le Comportement :moveupetmovedownqui permettent de modifier l’ordre des catégories partageant le même parent.admindelete: nous appelons la méthoderemoveFromTreeajoutée par le Comportement, qui se charge de mettre l’arborescence à jour. Si le deuxième paramètre vaut true, le noeud $id est réellement supprimé et ses enfants sont réaffectés au noeud parent du noeud $id (ou à la racine de l’arbre si le noeud $id avait la racine pour parent). S’il vaut false, le noeud n’est pas supprimé mais son nouveau parent est la racine, et ses enfants sont réaffectés au noeud parent de $id. Pour supprimer à la fois le noeud $id et TOUS ses enfants, la méthodedelete()habituelle est tout indiquée.
Nous verrons très prochainement comme présenter une telle arborescence, tout d’abord de manière statique avec l’affichage des catégories filles au fur et à mesure de la progression dans l’arbre, puis de manière dynamique où la navigation dans l’arbre se fera en javascript avec jQuery, sans recharger la page.
Commentaires
15 décembre 2008 à 17:59
Bonjour,
alors très bon tuto, comme d’habitude devrais-je dire;-)
tout fonctionne à merveille, sauf que parfois à l’édition d’une entrée, blam, apache plante, mais l’entrée est quand même éditée ou créée selon…
une idée peut-être???
16 décembre 2008 à 9:01
Effectivement je me souviens moi aussi avoir fait planter Apache, quand je ne passais pas le nom du Modèle arborescent dans la méthode TreeHelper::generate().
Attention donc, si tu appelles la méthode $tree->generate() ailleurs que dans une vue gérée par le Contrôleur dont le Modèle est arborescent, tu dois impérativement passer le nom du Modèle concerné à la méthode generate :
2 mars 2009 à 14:26
Bonjour, merci pour ce tuto trés instructif pour un débutant comme moi, mais je fais planté le système à chaque insertion ou mise à jour. Les données sont bien insérer ou mise à jour malgrés tout. Avez vous une idée pour me faire avancé? (j’ai suivi scrupuleusement le tuto et j’utilise la version cake_1.2.1.8004)
2 mars 2009 à 15:39
As-tu essayé l’astuce ci-dessus ?
2 mars 2009 à 15:54
merci pour cette info, je vais essayer de suite
7 mai 2009 à 17:41
Bonjour,
J’avais la même erreur, apache qui plante à chaque fois que j’envoie une donnée :/
J’ai trouvé la solution, à première vue c’est le setFlash qui déconne dans le controller et dans la méthode edit.
Il faut le remplacer par cela : $this->Session->setFlash(« Les données sont correctement enregistrées »);
9 mai 2009 à 10:51
Dans le tutoriel, la méthode setFlash prend en deuxième paramètre un nom de layout de message, par exemple ‘messageok’. Effectivement si le fichier correspondant, ici app/views/layouts/messageok.ctp n’existe pas, Cake plante.
5 juin 2009 à 9:49
Hello,
alors oui, effectivement, il s’agissait seulement du layout du setFlash…
excellent tuto… comme d’habitude devrais-je dire
5 juin 2009 à 11:10
Bonjour,
voilà, je voudrais gérer avec le tree behavior une série de rubriques et de pages attachées à ces rubriques…
Donc en suivant ce tuto, aucun soucis pour gérer les rubriques et sous rubriques… mais comment faire pour gérer selon ce même principe les pages attenantes?
dois-je créer une entité « page » (un modèle), définissant aussi bien une rubrique qu’une page, ou puis-je étendre ce tree à plusieurs modèles différents, donc rubrique et page, les liant via ce beahavior?
merci d’avance et surtout bon week end;-)
6 juillet 2009 à 17:03
Excellent ! Petite correction quand même pour la suppression il faut mettre :
Ainsi les enfants seront automatiquement supprimés. Cela évite de se retrouver avec des orphelins et plein d’incohérence.
D’ailleurs c’est une astuce très utile pour supprimer toutes les dépendances sans erreurs…
25 novembre 2009 à 20:41
Les messages lors du déplacement de la catégorie vers le haut ou le bas ne s’affiche pas chez moi. (pas de plantage et le layout pour le message existe bien)
Mais en fait je vois mal comment il peuvent s’afficher du fait que l’on fait un retour sur la page précédente à la fin du traitement.
26 novembre 2009 à 14:06
Bon lancé dans un projet cakehp, je regarde et que vois je ? De magnifiques tuto.
Y a juste un problème c’est qu’en faisant tout comme dit dans ce tuto avec de grands copier coller, a chaque fois que je lançais une action, apache tombait en carafe avec déconnexion etc…
Alors je mets ma truffe de fin limier codeur et en fait je m’aperçois que ce qui fout en l’air tant de travail c’est :
Enfin bref tous les commentaires de succès echec du tuto me foutent le bronx.
Donc ma solution a été de virer le second paramètre qui ne sert à rien dans le setflash pour avoir ceci :
26 novembre 2009 à 14:11
Apport personnel, pour la suppression utiliser la fonction :
Permet en fonction du second paramètre de spécifier si les sous catégories sont réaffectée ou non. Mieux que del tout simple
26 novembre 2009 à 15:26
Le deuxième paramètre de la méthode setFlash est le nom d’un layout à utiliser en fonction du type de message. Si tu n’as pas créé le layout correspondant, par exemple /views/layouts/message_notice.ctp, ça ne peut pas fonctionner.
26 novembre 2009 à 15:26
Tout à fait ! Je mets à jour l’article, merci.
26 novembre 2009 à 15:38
@mibs : setFlash enregistre un message dans la session, et arrivé à destination (après un resirect par exemple) ce message est affiché si on a quelque part dans la vue (layout ou vue elle-meme) : < ?php $session->flash(); ?>, sinon le message est conservé en session.