Pagination des données dans une relation HABTM
Il est possible depuis peu d’ajouter un nouveau paramètre dans le tableau d’options des méthodes Model::find() et Controller::paginate(). Cette nouvelle clé, 'joins', permet de définir facilement des restrictions sur une jointure HABTM et de paginer les données tout aussi facilement. Imaginons une petite application de recettes classées par catégories, une recette pouvant appartenir à plusieurs catégories et une catégorie pouvant être associée à plusieurs recettes.
1. Définition des modèles
Notre base de données contient trois tables : categories, recettes et la table de liaison categories_recettes contenant deux champs : category_id et recette_id.
Nous n’allons définir que deux modèles, Category et Recette, en laissant CakePHP gérer seul la liaison.
// {app}/models/category.php class Category extends AppModel { var $displayField = 'nom'; var $hasAndBelongsToMany = array('Recette'); } // {app}/models/recette.php class Recette extends AppModel { var $hasAndBelongsToMany = array('Category'); }
Notons bien que dans la mesure où nous avons respecté les conventions de nommage de CakePHP, à la fois pour le nom des tables et pour le nom des champs, nous n’avons que le strict minimum à déclarer pour créer notre association.
2. Pagination classique
Pour afficher les recettes, nous pouvons commencer par paginer simplement les données dans l’action index du Contrôleur RecettesController :
// {app}/controllers/recettes_controller.php class RecettesController extends AppController { // Options de pagination par défaut : var $paginate = array( 'order' => 'Recette.created DESC', 'limit' => 10 ); function index() { $this->set('recettes', $this->paginate()); } }
La vue correspondante boucle sur le tableau $recettes, rien de difficile ici. Nous pouvons naviguer dans les différentes pages et classer les recettes selon l’une des colonnes de la table.
3. Pagination avec restriction
Nous souhaiterions avoir le confort de classer nos recettes par catégorie, tout en conservant la pagination et la possibilité de changer le critère de tri. Nous allons donc ajouter dans la vue une liste déroulante des catégories, et le choix d’une de ces catégories renverra sur la même page mais avec un id de catégorie en paramètre.
3.1 Liste des catégories
Commençons par modifier l’action index() pour transmettre la liste des catégories existantes à la vue :
// {app}/controllers/recettes_controller.php function index() { $this->set('categories', $this->Recette->Category->find('list')); $this->set('recettes', $this->paginate()); }
Nous pouvons maintenant créer la liste déroulante des catégories dans la vue :
// {app}/views/recettes/index.ctp
<form action="">
<div class="input">
<label for="filter">Catégorie :</label>
<select id="filter" onChange="document.location=this.options[this.selectedIndex].value">
<option value="/recettes/index">Toutes</option>
<?php foreach($categories as $id => $nom) {
$selected = (isset($this->params['pass'][0]) && $this->params['pass'][0] == $id)
? ' selected="selected"'
: '';
echo '<option value="/recettes/index/'.$id.'"'.$selected.'>'.$nom."</option>\n";
} ?>
</select>
</div>
</form>3.2 Prise en compte du critère de restriction
Le choix d’une catégorie dans cette liste nous envoie bien sur l’URL /recettes/index/ suivie d’un id de catégorie, il nous faut maintenant modifier l’action index() pour qu’elle prenne en compte ce paramètre dans les critères de pagination :
// {app}/controllers/recettes_controller.php function index($category_id = null) { if (!empty($category_id)) { $this->paginate['joins'] = array( array( 'table' => 'categories_recettes', 'alias' => 'CategoryRecettes', 'type' => 'inner', 'conditions'=> array('CategoryRecettes.recette_id = Recette.id') ), array( 'table' => 'categories', 'alias' => 'Category', 'type' => 'inner', 'conditions'=> array( 'Category.id = CategoryRecettes.category_id', 'Category.id' => $category_id ) ) ); } $this->set('categories', $this->Recette->Category->find('list')); $this->set('recettes', $this->paginate()); }
Nous nous servons de la nouvelle clé 'joins' du tableau d’options utilisé par la méthode paginate(), en lui passant un tableau à deux entrées :
- les informations sur la relation avec la table de liaison,
categories_recettes:- le nom de la table ;
- le nom du modèle, même s’il n’existe pas physiquement de fichier correspondant dans le répertoire {app}/models ;
- le type de jointure que devra faire la base de données entre les deux tables ;
- la correspondance entre la clé primaire de la table
recetteset la clé étrangère de la table pivot.
- les informations sur la relation entre la table de liaison,
categories_recetteset la tablecategories:- le nom de la table ;
- le nom du modèle ;
- le type de jointure ;
- la correspondance entre la clé primaire de la table
categorieset la clé étrangère de la table pivot, ainsi que le critère de restriction sur l’id de la catégorie qui doit être égal à celui passé en paramètre de l’action.
Quand nous choisissons une catégorie, la liste des recettes se met bien à jour, mais il nous reste un dernier détail à régler pour que l’id de catégorie choisi soit transmis quand nous changeons de page ou quand nous changeons un critère de tri. Nous devons transmettre les arguments passés en paramètre de l’action (ici, il s’agit de l’id de catégorie) au Helper Paginator de la façon suivante, quelque part au début de la vue :
// {app}/views/recettes/index.ctp $paginator->options(array('url' => $this->passedArgs));
Note : cela est automatique depuis la version 1.3.
Cette fois, si nous choisissons une catégorie dans la liste déroulante, le listing est bien mis à jour, et nous pouvons effectivement parcourir les différentes pages et/ou choisir un critère de tri.
Commentaires
13 février 2009 à 9:04
[...] Pagination des données dans une relation HABTM [...]
15 février 2009 à 15:38
Bonjour,
Excellent tuto comme d’habitude….
J’ai un cas d’étude un peu similaire mais avec une table supplémentaire ‘Appreciation’. Supposons que cette table dépende à la fois de la catégorie et de la recette. Doit-on ajouter un champ id dans la table de jointure categories_recettes?
Doit-on créer un modèle pour cette table?
10 juin 2009 à 15:05
Salut kalt, merci pour ce tuto, vachement interessant.
Néanmoins j’ai un petit soucis: Pour ma part, je transmet l’id du filtre à l’aide d’un formulaire, dans l’evenement onChange du select, je fais un submit. Du coup dans mon action index (qui n’a aucun parametre), je teste: $this->data['Filter']. Cela fonctionne tres bien, sauf qu’il y a un petit soucis:
Lorsque je tri mes recettes, donc je suis dans une certaine categorie, puis je demande un tri, ehh bin $this->data['Filter'] ne contient pas l’id du filtre ! comment je pourrai valider le formulaire au moment du tri ? Est il possible de detecter si on a demandé un tri au paginator ? Ca me permettra de renvoyer le formulaire et donc avoir l’id du filtre dans $this->data['Filter'].
Merci
10 juin 2009 à 18:31
« valider le formulaire au moment du tri » : ça me paraît extrêmement compliqué. Si tu persistes à fonctionner en POST et non en GET comme dans le tutoriel, ma première idée serait de ne pas proposer le tri sous forme de liens dans les entêtes de colonnes, mais sous forme d’une deuxième liste déroulante dans ton formulaire qui propose de choisir une catégorie. Au moment de la soumission de ce formulaire, tu auras donc toujours un id de catégorie (ou null) et un ordre de tri.
Par contre je ne saisis absolument pas comment tu passes les arguments d’une page à l’autre avec ta méthode…
18 mai 2010 à 10:28
bonjour, j’ai une question : comment ferait on pour rechercher une recette avec 2 catégories (ou plus),
cet à dire un formulaire ou l’on le choix d’entrer 2 catégories et les recettes affichées sont celle qui appartiennent bien à ces 2 catégories.
j’aimerai faire quelque chose comme cela
merci d’avance
Jonathan
26 mai 2010 à 7:43
Au lieu de passer un seul id de catégories dans les options du paginate, il suffit de passer un array avec plusieurs id de catégorie.
26 mai 2010 à 8:26
Bonjour, merci pour la réponse,
mais le problème, c’est que si une recette à plusieurs catégories, et que 2 de ces catégories(ou plus) apparaissent dans les options du paginate, hé bin… elle apparait 2 fois(ou plus) dans les résultats…
est-ce que vous comprenez mon problème?
20 janvier 2012 à 16:12
Bonjour
Je suis actuellement en train de refaire mon site web mais je suis dans une impasse.
Mon probleme est similaire au commentaire de Jonathan. Je récupère bien mes articles qui n’ont pas le tag « Portfolio » (ID 6). Mais dès qu’un article a plus d’un tag alors il se retrouve en double.
Comment règler ce problème ?
Merci
25 janvier 2012 à 16:21
Ajoute
$this->paginate['group'] = 'Recette.id';