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 :

  1. 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 recettes et la clé étrangère de la table pivot.
  2. les informations sur la relation entre la table de liaison, categories_recettes et la table categories :
    • le nom de la table ;
    • le nom du modèle ;
    • le type de jointure ;
    • la correspondance entre la clé primaire de la table categories et 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.

Pierre-Emmanuel Fringant

Articles connexes

Commentaires

[...] Pagination des données dans une relation HABTM [...]

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?

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

« 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…

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

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.

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?

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

Ajoute $this->paginate['group'] = 'Recette.id';

Participez

Pour insérer une portion de code, utilisez <pre lang="php">...</pre>