How-to Symfony: Gestion d’un arbre en Propel via les NestedSet – Part 2

system-run 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)) ?>
[...]

Le résultat nous donne ceci:
image-311

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;
}

Et la magie opère de nouveau:
image-331

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:
image-34

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: saveOrder

Et 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.

On obtient donc:
image-351

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: , , , , , , ,

A propos de l'auteur

Tim

Développeur web spécialisé Symfony, il est avant tout passionné de web tout simplement. Il aime les défis et farfouiller dans le code de Symfony ou Doctrine. Fondateur du blog, il exerce chez Autrement.

Vous avez aimé ce billet? Faites le savoir!

  • Delicious
  • Twitter
  • Technorati Favorites
  • FriendFeed
  • Google Bookmarks
  • Share/Bookmark

11 Réponses

  1. Lionel 20 mai 2009 à 7 h 56 min #

    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!

  2. Tim 20 mai 2009 à 8 h 55 min #

    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 ;)

  3. netounet 21 mai 2009 à 14 h 27 min #

    Excellent tuto qui m’a bien inspiré.

    Merci

  4. Alban Duval 7 septembre 2009 à 11 h 31 min #

    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é.

  5. Tim 7 septembre 2009 à 14 h 28 min #

    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 : )

  6. tauruz 9 novembre 2009 à 23 h 40 min #

    How to create a new root element?

  7. Tim 10 novembre 2009 à 0 h 29 min #

    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

  8. Ukraine 17 novembre 2009 à 5 h 02 min #

    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!

  9. tauruz 25 novembre 2009 à 23 h 05 min #

    Hi.

    After repeated sorting tree is broken. What?

  10. nico 29 novembre 2009 à 16 h 40 min #

    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.

  11. Tim 30 novembre 2009 à 11 h 55 min #

    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 : )


Laisser un message