How-to Symfony: Gestion d’un arbre en Propel via les NestedSet – Part 2
La dernière fois, on avait vu comment construire l’aspect graphique de notre gestion de catégorie via Symfony et Propel. On va voir maintenant comme lui associer les actions symfony qui vont bien.
Au programme donc dans l’ordre:
- la remise en état des liens modifier/supprimer
- l’ajout d’un lien pour ajouter un enfant à une catégorie
- la sauvegarde de l’ordre de l’arbre
Ya du boulot, alors on se lance.
Le seul problème à la remise des liens modifier/supprimer c’est leur structure HTML, en ul/li qui pose souci avec le plugin NestedSortable. On va donc revoir la chose en les mettant dans un « p » avec de simples liens. Pour cela on va éditer dans notre module le fichier lib/categoriesGeneratorHelper.class.php (qui doit être vierge pour l’instant) et on va surcharger les méthodes, linkToEdit et linkToDelete:
public function linkToEdit($object, $params) { return '<span class="sf_admin_action_edit">'.link_to(__($params['label'], array(), 'sf_admin'), $this->getUrlForAction('edit'), $object).'</span>'; } public function linkToDelete($object, $params) { if ($object->isNew()) { return ''; } return '<span class="sf_admin_action_delete">'.link_to(__($params['label'], array(), 'sf_admin'), $this->getUrlForAction('delete'), $object, array('method' => 'delete', 'confirm' => !empty($params['confirm']) ? __($params['confirm'], array(), 'sf_admin') : $params['confirm'])).'</span>'; }
Les seules modifications concernant le changement d’un li par un span.
Maintenant on va modifier le partial _list_td_actions.php et donc le rajouter dans notre répertoire templates de notre module:
<p class="sf_admin_td_actions list_action">
<span class="sf_admin_action_new">
<?php echo link_to(__('Ajout Enfant'), 'categories/addChild?id='.$categories->getId(), array(), 'messages') ?>
</span>
<?php echo $helper->linkToEdit($categories, array( 'params' => array( ), 'class_suffix' => 'edit', 'label' => 'Edit',)) ?>
<?php echo $helper->linkToDelete($categories, array( 'params' => array( ), 'confirm' => 'Are you sure?', 'class_suffix' => 'delete', 'label' => 'Delete',)) ?>
</p>A noter qu’on a ajouté au passage l’action Ajout d’enfant.
Il nous reste plus qu’à rajouter l’appel à ce partial dans notre _tree.php:
[...] <div class="sort-handle"> <?php echo $node->getTitre() ?> </div> <?php include_partial('categories/list_td_actions', array('categories' => $node, 'helper' => $helper)) ?> [...]
Alors oui c’est moche, mais ca fonctionne! Maintenant, on passe un petit coup de CSS comme d’habitude:
.list_action { position: absolute; right: 0; top:0; width: 350px; } .list_action span { padding: 0 10px 0 0; } .current-nesting { background-color: #FFE; } #sf_admin_container #sf_admin_content p span.sf_admin_action_new a { background:transparent url(/sfPropelPlugin/images/new.png) no-repeat scroll 0 0; } #sf_admin_container #sf_admin_content p span.sf_admin_action_edit a { background:transparent url(/sfPropelPlugin/images/edit.png) no-repeat scroll 0 0; } #sf_admin_container #sf_admin_content p span.sf_admin_action_delete a,.link_delete { background:transparent url(/sfPropelPlugin/images/delete.png) no-repeat scroll 0 0; }
On va maintenant créer les actions liées à cette nouvelle action dans notre fichier d’actions de notre module:
public function executeAddChild(sfWebRequest $request) { $this->categories = $this->getRoute()->getObject(); // On récupère le parent pour insérer la nouvelle catégorie sous ce parent et on le fait transiter via session $this->getUser()->setAttribute('categories.addChild',$this->categories->getPrimaryKey(), 'admin_module'); $this->form = $this->configuration->getForm(); $this->setTemplate('new'); } /** * On surcharge le processForm pour récupérer le parent de la catégorie à ajouter s'il existe * * @param sfWebRequest $request * @param sfForm $form */ protected function processForm(sfWebRequest $request, sfForm $form) { if ($parent = $this->getUser()->getAttribute('categories.addChild', null, 'admin_module')) { $this->getUser()->getAttributeHolder()->remove('categories.addChild', null, 'admin_module'); $root = CategoriesPeer::retrieveByPk($parent); $form->getObject()->insertAsLastChildOf($root); } parent::processForm($request,$form); }
Si vous cliquez maintenant sur « Ajouter un enfant’ d’une des catégories déjà présente vous obtenez alors:

Vu que les tree left et tree right sont définis par Propel, on va les supprimer du formulaire.
Dans notre CategoriesForm.class.php on rajoute:
public function configure() { unset( $this['tree_left'], $this['tree_right'] ); }
Maintenant, il ne manque plus qu’à pouvoir sauvegarder les modifications d’ordre effectuées via javascript. Pour cela on va rajouter une batch actions (elles soumettent un formulaire, il sera ainsi plus facile de faire passer les modifications faites à l’arbre).
Dans notre generator.yml on rajoute dans l’entrée list:
batch_actions:
saveOrder:
label: Sauvegarder ordre
action: saveOrderEt dans notre partial _list.php, juste avant le javascript:
<div> <input type="hidden" id="hashValue" name="hashValue" value="" /> <input type="hidden" name="ids" value="<?php echo (!empty($item)?$item->getPrimaryKey():'1') ?>" /> </div>
Le premier pour récupérer la sérialisation de la nestedlist en javascript, le deuxième pour passer outre le système de batch qui demande de sélectionner au moins un ids pour lancer l’action ensuite.
Il ne reste plus qu’à faire l’action correspondante dans notre fichier d’actions:
public function executeBatchSaveOrder(sfWebRequest $request) { $hash = $request->getParameter('hashValue'); parse_str($hash, $list); CategoriesPeer::saveOrder($list); $this->getUser()->setFlash('notice', 'Ordre mis à jour'); }
Et les fonctions de sauvegarde dans notre modèle. La fonction mère dans le CategoriesPeer.php:
public static function saveOrder($hash) { $root = self::retrieveRoot(); foreach ($hash['main_list'] as $categorie) { $u = self::retrieveByPk($categorie['id']); if (!empty($categorie['children'])) { $u->saveChildren($categorie['children']); } $u->moveToLastChildOf($root); $u->save(); } }
Qui gère les « racines » et son équivalent récursif dans le model Categories.php:
public function saveChildren($children) { foreach($children as $child) { $item = CategoriesPeer::retrieveByPk($child['id']); if (!empty($child['children'])) { $item->saveChildren($child['children']); } $item->moveToLastChildOf($this); $item->save(); } }
Et voilà! Vous pouvez maintenant déplacer vos catégories et sauvegarder leur position dans l’arbre.
Evidemment toute question, suggestion sont bien sûr les bienvenues. Je pense mettre les source et une démo en ligne très bientôt, voir en faire un plugin si jamais je rencontre un réel intérêt pour ce genre de gadget.
En attendant, une petit illustration en vidéo:
Tags: admin, howto, javascript, jquery, nestedset, PHP, propel, Symfony
11 Réponses
Laisser un message




salut,
merci pour ce tuto!
Par contre, un enregistrement en ajax de l’ordre de l’arbre, au moment du déplacement d’un élément, serait plus convivial!
Hello,
Effectivement c’est une idée mais le plugin jquery ne permet pas de faire des sauvegarde à la volée, il retourne seulement l’arbre entier sérialisé.
Son développeur indique qu’un déplacement pouvant modifier plus d’un élément de l’arbre.
Et l’opération de sauvegarde étant un peu lourde selon l’importance de l’arbre, je ne sais pas si ca aiderait vraiment.
Mais je garde l’idée dans un coin ;)
Excellent tuto qui m’a bien inspiré.
Merci
Hello,
Merveilleux tutorial, il m’a économisé pas mal de temps !
Par contre, j’ai quand même trouvé un petit souci :P
L’attribut définit lors du clic sur ‘Ajout enfant’ peut poser problème si la procédure d’ajout n’est pas menée à terme. On se retrouve avec un attribut qui va empêcher l’édition d’une autre catégorie.
Je conseille donc de surcharger l’action « edit » comme ceci:
public function executeEdit(sfWebRequest $request)
{
$this->getUser()->getAttributeHolder()->remove(‘gallery.addChild’, null, ‘admin_module’);
parent::executeEdit($request);
}
De cette façon, l’attribut est supprimé à chaque accès à la page d’édition, permettant la modification sans être pollué par l’attribut suscité.
Effectivement! De toute façon, le code est bien évidemment perfectible, et adaptable pour vous.
Mais c’est une possibilité qui méritait d’être soulevée, merci à toi : )
How to create a new root element?
Hello tauruz,
In fact, a node in a root cannot be moved into another root. So I decided to manage only one root.
However, if you defined many roots possible in your schema, you can use createRoot($node) to create a new root.
Hope it helps
May send someone in a finished application? French absolutely don `t know and with the symfony of work recently. My email rusya at mail.ru
Thank you very much!
Hi.
After repeated sorting tree is broken. What?
Excellent tuto, mais il me semble avoir trouvé un petit soucis. J’ai appliqué à la lettre ce tuto ainsi que le précédent. J’ai ensuite généré un arbre de 678 éléments (provenant d’une appli rééelle). Tout fonctionne bien sauf la sauvegarde de l’arbre qui explose la mémoire du processus d’apache, même en augmentant sa limite à 1Gb.
J’ai pensé que cela venait de la fonction récursive Categories.php::saveChildren(), mais même en la réécrivant de façon itérative, j’obtiens le même échec!
Dommage.
Oui, j’ai rencontré des difficultés similaires sur de gros arbres.
J’ai jamais eu le temps de me pencher sur le souci de performance, peut-être un jour : )