Gestion automatique d’une colonne de tri avec un Comportement
Nous avons souvent besoin d’une colonne de tri numérique dans une table, lorsque les données de cette table ne peuvent être classées selon un ordre standard, comme l’ordre alphabétique ou la date de création. C’est pourquoi nous avons créé un Comportement (Behavior) pour gérer automatiquement les opérations de maintenance de l’index de tri en cas d’ajout, modification ou suppression de données. Le Comportement ajoute également trois méthodes au Modèle associé, moveUp et moveDown, pour monter ou descendre un enregistrement de la table, et moveTo pour mettre à jour l’indice d’un enregistrement.
1. Installation
Mise à jour du 02/07/08 : le Comportement a été mis à jour pour être compatible avec la dernière version de CakePHP, qui introduit une nouvelle syntaxe pour les opérateurs SQL (depuis la RC1). Une simple condition comme :$field => ' >= ' . $value
devient
"$field >= " => $value
Pour installer le Comportement, il suffit de télécharger le fichier ci-dessous dans le répertoire {app}/models/behaviors/ et de le renommer en ordonnable.php.
Télécharger le Comportement OrdonnableBehavior
Ou de recopier le code ci-dessous et de l’enregistrer sous {app}/models/behaviors/ordonnable.php :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | <?php /** * Fichier de la classe OrdonnableBehavior - 16 mai 2008. * * @filesource * @author Pierre-Emmanuel Fringant * @link http://www.formation-cakephp.com/ * @version 0.3 * @license http://www.opensource.org/licenses/mit-license.php The MIT License * @package app * @subpackage app.models.behaviors */ /** * Comportement de modèle pour prendre en charge automatiquement * une colone de tri numérique. * * @package app * @subpackage app.models.behaviors */ class OrdonnableBehavior extends ModelBehavior { /** * Initialisation du Comportement avec les options par défaut. * * Options disponibles : * - field : nom du champ numérique de la base qui va contenir les valeurs de l'indice * - addToEnd : booléen, si vrai un nouvel enregistrement sera ajouté au dernier rang de l'index d'ordre, * si faux le nouvel enregistrement sera ajouté au premier rang. * * @param object $model Le modèle sur lequel va être appliqué le Comportement * @param array $config Les options définies dans le modèle (prioritaires sur les options par défaut) */ function setup(&$model, $config = array()) { $settings = array_merge( array( 'field' => 'ordre', 'addToEnd' => true ), (array)$config ); $this->settings[$model->alias] = $settings; } /** * Méthode appelée après chaque sauvegarde. * Si c'est un ajout, on regarde la valeur de l'option $addToEnd * et on met à jour la valeur de l'ordre de l'enregistrement concerné. * * @param object $model Le modèle relié au Comportement * @param boolean $created Vrai s'il s'agit d'un ajout (INSERT), faux si c'est une mise à jour (UPDATE) * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function afterSave(&$model, $created) { extract($this->settings[$model->alias]); if(!$created) { return; } if($addToEnd) { // Indice le plus élevé list($order) = array_values( $model->find( 'first', array( 'conditions' => '1=1', 'fields' => 'MAX(' . $field . ') AS ' . $field, 'recursive' => -1 ) ) ); // Nouvel indice $new = empty($order[$field]) ? 1 : $order[$field]+1; } else { // Décalage de tous les indices (+1 à tous) $this->__offset($model); // Nouvel indice $new = 1; } return $this->__updateOrder($model, $model->id, $new); } /** * Méthode appelée avant chaque suppression. * On décale d'un cran les enregistrements suivant celui à supprimer * * @param object $model Le modèle relié au Comportement * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function beforeDelete(&$model) { extract($this->settings[$model->alias]); $start = $model->field($field); return $this->__offset($model, $start, -1); } /** * Monter d'un certain nombre de crans * * @param object $model Modèle relié au Comportement * @param integer $id Id de l'enregistrement à monter * @param integer $delta Nombre de crans * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function moveUp(&$model, $id = null, $delta = -1) { return $this->__move($model, $id, $delta); } /** * Descendre d'un certain nombre de crans * * @param object $model Modèle relié au Comportement * @param integer $id Id de l'enregistrement à descendre * @param integer $delta Nombre de crans * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function moveDown(&$model, $id = null, $delta = +1) { return $this->__move($model, $id, $delta); } /** * Affecter un nouvel indice à un enregistrement * * @param object $model Modèle relié au Comportement * @param integer $id Id de l'enregistrement à descendre * @param integer $new Nouvel indice * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function moveTo(&$model, $id = null, $new = 1) { extract($this->settings[$model->alias]); $model->id = $id; $old = $model->field($field); if($new <= 0) { $new = 1; } return $this->__move($model, $id, $new - $old); } /** * Monter ou descendre d'un certain nombre de crans * * @param object $model Modèle relié au Comportement * @param integer $id Id de l'enregistrement à modifier * @param integer $delta Nombre de crans * @return boolean Renvoie vrai si succès, faux en cas d'erreur * @access private */ function __move(&$model, $id = null, $delta = +1) { extract($this->settings[$model->alias]); $model->id = $id; $old = $model->field($field); $new = $old + $delta + ($delta > 0); if($new <= 0) { return false; } $return = $this->__offset($model, $new); if($return) { $return = $this->__updateOrder($model, $id, $new); } if($return) { $return = $this->__reIndex($model); } return $return; } /** * Appliquer un décalage de plusieurs lignes à partir d'un indice. * * @param object $model Modèle relié au Comportement * @param integer $start Indice à partir duquel appliquer le décalage * @param integer $offset Grandeur du décalage * @return boolean Renvoie vrai si succès, faux en cas d'erreur * @access private */ function __offset(&$model, $start = 1, $offset = +1) { extract($this->settings[$model->alias]); return $model->updateAll( array( $model->escapeField($field) => $model->escapeField($field) . '+' . $offset ), array( $model->escapeField($field) . " >= " => $start ) ); } /** * Réindexer la colonne de tri. * * @param object $model Modèle relié au Comportement * @param integer $start Indice à partir duquel réinitialiser le compteur * @return boolean Renvoie toujours vrai * @access private */ function __reIndex(&$model, $start = 1) { extract($this->settings[$model->alias]); $fromStart = $model->find( 'all', array( 'fields' => array( $model->primaryKey, $field ), 'conditions' => array( "$field >= " => $start ), 'order' => array( $field => 'asc' ), 'recursive' => -1 ) ); foreach($fromStart as $row) { if($row[$model->alias][$field] != $start) { $this->__updateOrder($model, $row[$model->alias][$model->primaryKey], $start); } $start++; } return true; } /** * Echanger les ordres de deux enregistrements * * @param object $model Modèle relié au Comportement * @param integer $one_id Id du premier enregistrement * @param integer $two_id Id du deuxième enregistrement * @return boolean Renvoie vrai si succès, faux en cas d'erreur * @access private */ function __switch(&$model, $one_id, $two_id) { extract($this->settings[$model->alias]); $one = $model->find( 'first', array( 'fields' => array( $model->primaryKey, $field ), 'conditions' => array( $model->primaryKey => $one_id ), 'recursive' => -1 ) ); $two = $model->find( 'first', array( 'fields' => array( $model->primaryKey, $field ), 'conditions' => array( $model->primaryKey => $two_id ), 'recursive' => -1 ) ); if(empty($one) or empty($two)) { return false; } list($one) = array_values($one); list($two) = array_values($two); $return = $model->__updateOrder($model, $one[$model->primaryKey], $two[$field]); if($return) { $return = $model->__updateOrder($model, $two[$model->primaryKey], $one[$field]); } return $return; } /** * Mise à jour de l'indice d'un enregistrement * * @param object $model Modèle relié au Comportement * @param integer $id Id de l'enregistrement * @param integer $order Nouvel indice * @param boolean $validate Vrai si on doit valider les données * @return boolean Renvoie vrai si succès, faux en cas d'erreur */ function __updateOrder(&$model, $id = null, $order = 0, $validate = false) { extract($this->settings[$model->alias]); $model->id = $id; return $model->saveField($field, $order, $validate); } } ?> |
2. Mise en pratique
Nous allons voir maintenant un exemple concret de l’utilisation de notre Comportement. Imaginons une gestion de diplômes : lorsque nous les affichons, nous souhaitons bien sûr les classer par ordre de niveau d’études, ce qui ne correspond ni à l’ordre de saisie ni à l’ordre alphabétique. Nous ajoutons donc une colonne de type numérique à notre table pour gérer l’ordre comme nous le souhaitons.
2.1. La table
Nous créons la table diplomes avec trois colonnes :id: la clé primaire ;libelle: le nom du diplôme ;ordre: nombre entier non signé qui va servir à conserver la liste des diplômes dans l’ordre voulu.
CREATE TABLE `diplomes` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `libelle` VARCHAR(50) NOT NULL, `ordre` INT(10) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`id`) )
2.2. Le Modèle
Nous créons le fichier {app}/models/diplome.php :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // {app}/models/diplome.php class Diplome extends AppModel { var $name = 'Diplome'; var $displayField = 'libelle'; var $actsAs = array( 'Ordonnable' => array( 'field' => 'ordre', 'addToEnd' => true ) ); } |
La variable $actsAs nous permet de dire au Modèle Diplome de se comporter comme étant « Ordonnable », et ce avec deux options :
field: la colonne de la table qui va conserver l’index de classement, ici notre colonne ‘ordre’. Vaut ‘ordre’ par défaut.addToEnd: si vrai, un nouvel enregistrement sera placé à la fin, si faux il sera placé au début. Vrai par défaut.
2.3. Le Controller
Voyons les quatre actions qui nous intéressent :index (liste des diplômes par ordre voulu), monter (monter un diplôme d’un cran dans la liste), descendre (descendre un diplôme d’un cran dans la liste) et deplacer pour mettre à jour directement l’indice d’un diplôme :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // {app}/controllers/diplomes_controller.php class DiplomesController extends AppController { var $name = 'Diplomes'; function index() { $diplomes = $this->Diplome->find( 'all', array('order' => 'Diplome.ordre ASC') ); $this->set(compact('diplomes')); } function monter($id = null) { if($id) { $this->Diplome->moveUp($id); $this->redirect('index'); } } function descendre($id = null) { if($id) { $this->Diplome->moveDown($id); $this->redirect('index'); } } function deplacer() { if(!empty($this->data)) { $this->Diplome->moveTo( $this->data['Diplome']['id'], $this->data['Diplome']['indice'] ); $this->redirect('index'); } } } |
Les méthodes monter et descendre appellent deux méthodes du Comportement OrdonnableBehavior, accessibles depuis le Modèle Diplome associé.
Il ne reste au lecteur qu’à créer les actions d’ajout, d’édition et de suppression d’un diplôme, mais ces actions sont tout à fait classiques et nous ne les présenterons pas ici.
Le Comportement se chargera automatiquement de maintenir l’index d’ordre à jour en cas d’ajout ou de suppression d’un enregistrement.
2.4. La Vue index
Finissons par la vue correspondant à l’actionindex du Contrôleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // {app}/views/diplomes/index.ctp
<?php
e($html->tableHeaders('Ordre actuel', 'Libellé', 'Monter', 'Descendre', 'Déplacer'));
foreach ($diplomes as $row): ?>
<tr>
<td><?php e($row['Diplome']['ordre']); ?></td>
<td><?php e($row['Diplome']['libelle']); ?></td>
<td><?php e($html->link("Monter", 'monter/'.$row['Diplome']['id'])); ?></td>
<td><?php e($html->link("Descendre", 'descendre/'.$row['Diplome']['id'])); ?></td>
<td>
<?php e($form->create('Diplome', array('action' => 'deplacer'))); ?>
<?php e($form->input('id', array('value' => $row['Diplome']['id']))); ?>
<?php e($form->input('indice', array('label' => false, 'div' => false, 'size' => 3, 'value' => $row['Diplome']['ordre']))); ?>
<?php e($form->submit('ok', array('div' => false))); ?>
<?php e($form->end()); ?>
</td>
</tr>
<?php endforeach; ?>
</table> |
Commentaires
18 janvier 2009 à 17:02
Salut Kalt,
Cela faisait un petit moment que je n’avais pas utilisé l’un de tes excellents tutos. Et comme à chaque fois ou presque, j’ai une interrogation.
Serait-il envisageable de gérer « plusieurs » ordres de tri ?
Je m’explique : dans mon appli, j’ai besoin de gérer un ordre d’affichage des articles au sein d’une même page. Or, tous les articles, quelle que soit leur page parente sont dans une même table, du coup, si j’ai 12 articles, dont 6 appartiennent à la page 1, le premier article de la page 2 aura comme « indice » 7 et non pas 1.
Dans l’absolu ce n’est pas grave puisque le ORDER BY dans la vue placera mes articles dans l’ordre que je veux, mais d’un point de vue utilisation, c’est pas évident de se dire : « je déplace d’un cran le numéro 7 pour qu’il devienne le 2e article de la page 2″ !
En espérant avoir été clair et dans l’attente de te lire à nouveau…
Aurélien
21 janvier 2009 à 14:14
Je ne pense pas qu’il soit bien compliqué de modifier mon Behavior pour que les méthodes de changement d’ordre acceptent en paramètre un array de conditions. Dans ton cas on pourrait ainsi appeler
$model->moveUp($id, array('pageid' => $pageid));21 janvier 2009 à 14:41
Oui je le pense aussi, mais parfois j’ai encore du mal avec les Behaviors… qui sont tout de même des classes un peu particulières puisqu’elles impactent complètement le modèle avec lequel elles interagissent.
Je vais donc regarder de plus près ce que tu m’indiques et je ferais un retour si ça marche.
Merci de ta disponibilité
8 février 2009 à 16:32
J’ai enfin pu faire quelques tests sur ma condition de page parente, en passant comme tu me le proposais, un tableau de conditions en plus de l’id.
J’ai réussi à modifier « moveUp », « moveDown » et « moveTo » et j’ai bien un ordre d’affichage en fonction de la page parente et non plus global.
Mais je ne sais pas trop comment passer la page parente aux méthodes « afterSave » (quand j’ajoute un nouvel article) et « beforeDelete »… A quel niveau dois-je agir pour que ces callbacks magiques connaissent ma variable ?
8 février 2009 à 16:45
Bon en fait j’ai trouvé !
Je viens de comprendre l’utilisation de « $model->field($field) » et du coup j’ai ajouté une 3eme variable à ton tableau de config du behavior !
Et du coup, je me dis qu’en utilisant cela, je peux me dispenser d’ajouter le tableau de conditions lors des appels à « moveUp » et consorts, donc d’ajouter ce paramètre à toutes les méthodes. Si j’ai bien suivi, je peux l’obtenir dans chaque méthode grâce à la méthode « extract() ».
Mais bon, si on envisage d’avoir bien plus d’un champ conditionnel… il faudra créer autant de nouvelle variable dans le tableau de config. Pas génial !
30 juin 2011 à 22:53
Très bon tuto comme souvent.
Par contre, il faut préciser qu’il ne faut SURTOUT PAS choisir comme nom de champ ‘order’ (ordre en anglais).
… en effet apparemment ce mot est réservé en SQL et la requête générée par le Behavior comporte alors une erreur de syntaxe.
Aussi, ne pas mettre le champ « ordre » dans le formulaire de création ou d’édition… car là aussi on obtient des erreurs.
Encore merci pour tous ces tutos, c’est un plaisir.