Aujourd’hui, voyons les fonctions sortables permettant de changer la position d’objet, de les ordonner et de sauvegarder l’ordre. C’est toujours pratique pour organiser une galerie photo, ou des éléments dans votre site internet.
Toutes les informations sur l’objet que nous souhaitons ordonner et/ou classer seront stockées en base :
schema.yml
Item:
columns:
name:
type: string(255)
rank:
type: integer
notnull: true
unique: true |
L’attribut ‘rank’ étant notnull, il faut forcément qu’il soit définit lors du save(), et étant unique on ne peut pas le définir par défaut.
Il faut donc surcharger la méthode save() :
Item.class.php
public function save(Doctrine_Connection $con = null) { // les nouveaux enregistrements seront ajoutés à la suite, donc avec un rang élévé de 1 par rapport au rang max if($this->isNew()) $this->setRank($this->getTable()->getMaxRank()+1); return parent::save($con); } |
Il faut aussi surchager la méthode delete() pour redescendre tous les éléments de rang supérieur lors de la suppression :
Item.class.php
public function delete(Doctrine_Connection $con = null) { //tous les enregistrements au dessus descendent d'une place $items = Doctrine_Query->create()->from('Item')->where('rank > ?', $this->getRank())->execute(); // delete the item $ret = parent::delete(); foreach($items as $item) { $item->setRank($item->getRank()-1); $item->save(); } return $ret; } |
Nous avons donc besoin d’une méthode permettant de récupérer le rang max :
ItemTable.class.php
//permet de récupérer le rang le plus haut public function getMaxRank() { $query = Doctrine_Query::create() ->select('MAX(rank)') ->from('Item i'); $tmp = $query->execute(array(),Doctrine::HYDRATE_NONE); return $tmp[0][0]; } |
Ici on passe en paramètre Doctrine::HYDRATE_NONE pour que la méthode ne retourne pas une collection. Le retour est un tableau à 2 entrées, mais la requête ne retournant qu’un résultat, il est disponible en [0][0].
Passons maintenant à la vue.
Il s’agit d’afficher la liste des items, tout simplement.
Mais pour les afficher, il faut les charger, voyons donc d’abord l’action :
actions.class.php
public function executeIndex() { $this->items = Doctrine::getTable('Item')->getAllOrderedByRank(); } |
Un coup d’oeil tout de suite sur la méthode getAllOrderedByRank() :
ItemTable.class.php
public function getAllOrderedByRank() { $query = Doctrine_Query::create() ->from('Item') ->orderBy('rank ASC'); return $query->execute(); } |
Maintenant voyons la vue :
indexSuccess.php
<h1>Ordered list of items</h1> <ul> <?php foreach($items as $item): ?> <li id="<?php echo $item->getId() ?>"> <?php echo $item->getName() ?> </li> <?php endforeach ?> </ul> |
Passons maintenant aux choses sérieuses.
Le framework Javascript JQuery comporte des fonctions permettant de créer des liste dont les éléments sont « drag n’ droppables », ici c’est celles ci que l’on va utiliser.
Pour cela, il faut modifier la vue, afin de rajouter le script jquery, et de spécifier une id à la liste, id que le script jquery utilisera pour cibler la liste à ordonner :
<ul id="order"> <?php foreach($items as $item): ?> <li id="<?php echo $item->getId() ?>"> <?php echo $item->getName() ?> </li> <?php endforeach ?> </ul> <input type="button" value="Enregistrer" onClick="submitOrder()"/> <script type="text/javascript"> $(function() { //on spécifie la liste $("#order").sortable(); $("#order").disableSelection(); }); function submitOrder() { //initialisation de la chaine contenant les id des items var elements = ""; //parcours de la liste d'items $('ul#order li').each(function() { //pour chaque item, on concatene l'id dans la chaine elements += $(this).attr('id')+","; }) //on execute l'action, a laquelle on envoie en param la liste des id $.post('<?php echo url_for('@order') ?>', { elements: elements.substr(0, elements.length-1) }); } </script> |
Vous aurez noté l’ajout du bouton submit. C’est en cliquant sur celui-ci que l’on déclenche l’action qui sauvegarde le nouvel ordre des items.
Pour enregistrer l’ordre des items, chaque
Lors du click on exécute un script qui récupère toutes les id dans l’ordre et qui les concatène dans une chaîne de caractères en les séparant par des « , ». Cette chaîne est envoyée en paramètre à l’action.
Routing :
routing.yml
order:
url: /order-item
params: { module: item, action: order } |
L’action :
actions.class.php
public function executeOrder(sfWebRequest $request) { //on récupère les id, que l'on place dans un tableau pour un parcours plus aisé $this->elements = explode(",",$request->getParameter('elements')); //on parcours les id, on récupère l'item correspondant, et on met son rang à jour foreach($this->elements as $key=>$element_id) { $item = Doctrine::getTable('Item')->find($element_id); $item->setRank($key); $item->save(); } $this->items = Doctrine::getTable('Item')->getAllByRank(); //on affiche à nouveau la liste des items, mis à jour $this->setTemplate('index'); } |
Voilà, vous aurez noté que les nouveaux rangs sont tout simplement la position dans la liste, partant de 0.
Dans la prochaine partie, nous verront comment implanter cette fonctionnalité dans votre backend avec l’admin generator

Le plugin csDoctrineActAsSortablePlugin marche déjà pas mal du tout, tu devrais y jeter un coup d’oeil.
http://www.symfony-project.org/plugins/csDoctrineActAsSortablePlugin
Pour la réorganisation d’éléments, il est toujours plus agréable de faire ça en drag & drop quand la liste est suffisamment courte.
Fabien avait rédiger un tutoriel sur la question, il est basé sur Prototype, mais les changements pour l’adapter à jQuery UI et autres sont minimes : http://www.symfony-project.org/cookbook/1_2/en/sortable
J’ai déjà lu le tuto de Fabien sur le CookBook, je me suis même basé dessus, mais il utilise une méthode du helper javascript, et sachant que les helpers javascript et form seront retirés de symfony sur la version 1.4 il fallait faire sans.
En revanche, j’avais pas pensé à chercher dans les plugins, et c’est vrai que celui dont tu donnes le lien à l’air puissant, et surtout ultra-pratique ! Merci.
Pour ceux que ça intéresse, j’ai testé ce plugin.
Il est effectivement très bien.
En réalité, c’est le contraction de tout ce qui est expliqué dans ce post dans un behavior, ce qui est excessivement pratique.
En revanche, il comporte des erreurs, en gros il n’a pas l’air fini.
J’en ai corrigé une ou deux, il fonctionne sur cette implémentation succinte, mais impossible de savoir s’il subsiste d’autres erreurs (des variables non définies dans les classes, des lettres manquantes : « beginTransation » au lieu de « beginTransaction »).
Je ferais probablement un petit post dessus prochainement.