Plugin de tags multi-modèles
Nous vous proposons un plugin permettant d’associer des tags à un ou plusieurs Modèles d’une application CakePHP. Nous avons souhaité que ce plugin ait les caractéristiques suivantes :
- Facile à installer : il suffit de télécharger le dossier et le placer dans le répertoire {app}/plugins, puis de créer deux tables quel que soit le nombre de Modèles à tagger.
- Pour pouvoir tagger un Modèle, il suffit d’ajouter un champ de type ‘text’ dans le formulaire d’ajout/édition d’un enregistrement. C’est tout. Nous proposons le choix entre un champ simple ou un champ qui propose des tags existants au fur et à mesure de la saisie.
- Le plugin inclut un Comportement (Behavior) qui ajoute des méthodes aux Modèles concernés : trouver les tags d’un enregistrement, trouver des enregistrements qui partagent le plus grand nombre de tags, etc.
- Construire et afficher un nuage de tags, avec un contrôle total de son contenu et de son apparence.
1. Installation
1.1. Fichiers
Le code source du plugin Tagging est hebergé sur Github.com :
http://github.com/kalt/tagging/tree/master
Deux méthodes pour installer les fichiers : soit en cliquant sur ‘download’ puis en extrayant le répertoire ‘tagging’ vers le répertoire ‘plugins’ de l’application, soit en exécutant la commande suivante (pour les familiers de Git) :
git clone git://github.com/kalt/tagging.git
1.2. Les tables de la base de données
Nous créons deux nouvelles tables dans notre base de données, soit selon le schéma décrit dans le fichier tagging/config/sql/tagging.sql, soit en exécutant la ligne de commande suivante dans la console de CakePHP :
cake schema run create Tagging -path plugins/tagging/config/sql
2. Mise en place
Voyons comment ajouter des tags à un enregistrement d’un Modèle, en utilisant un Behavior et un Helper inclus dans le plugin.
2.1. Dans le Modèle
class Article extends AppModel { var $actsAs = array('Tagging.Taggable'); }
2.2. Dans le Contrôleur
class ArticlesController extends AppController { var $helpers = array('Tagging.Tagging'); }
2.3. Dans la Vue
Nous ajoutons un seul champ pour permettre de saisir les tags séparés par une virgule.
2.3.a. Champ texte simple
echo $form->input('tags');
2.3.b. Champ texte avec aide à la saisie
echo $tagging->input('tags');
Cette méthode requiert l’inclusion de la librairie jQuery entre head.../head dans le layout.
C’est terminé, nous pouvons ajouter des tags aux enregistrements d’un ou plusieurs modèles de notre application !
2.3.c. Options disponibles pour l’aide à la saisie
Si nous optons pour cette solution, des tags existants nous seront suggérés au fur et à mesure de la saisie.
Les options de ce champ spécial se changent comme suit :
$tagging->options(array('option_key' => 'value', ...));
Les options par défaut étant :
selector: élément du DOM à observer (rester simple, juste une class (‘.xyz’) ou un identifiant (‘#xyz’)). Par défaut : ‘.tagSuggest’.url: url appelée en requête Ajax POST pour renvoyer les suggestions de tags (la réponse est renvoyée au format JSON). Par défaut : ‘/tagging/tags/suggest’.delay: délai entre la frappe d’une touche et l’envoi de la requête Ajax (en millisecondes). Par défaut : 500.matchClass: classe CSS appliquée aux suggestions. Par défaut : ‘tagMatches’.sort: booléen pour forcer le tri des suggestions. Par défaut : false.tagContainer: le type de l’élément utilisé pour envelopper un tag suggéré. Par défaut : ‘span’.tagWrap: le type de l’élément utilisé pour envelopper l’ensemble des suggestions. Par défaut : ‘span’.tags: tableau associatif de tags possibles (dans le cas où l’on ne veut pas utiliser l’ajax mais une liste prédéfinie). Par défaut : null.
Pour changer l’apparence par défaut des tags suggérés, nous donnons un autre nom de classe CSS à l’option ‘matchClass’, et nous ajoutons nos propres règles dans la feuille de style de l’application.
Le second paramètre de la méthode $tagging->input($fieldName, $options) peut être un tableau d’options, exactement comme pour un champ de type ‘text’ habituel, $form->input($fieldName, $options).
3. Gestion des Tags
Commençons par activer les routes ‘admin’ dans {app}/config/core.php.
Nous pouvons ensuite nous rendre à l’URL /admin/tagging: nous avons le choix entre ajouter un tag et voir la liste des tags.
Les vues de ces actions d’administration sont très simples, elles ont été générées par le script ‘bake’ de la console.
Si ces vues ne conviennent pas, il suffit de créer les nôtres directement dans le répertoire de notre application (sans rien toucher au plugin), dans le répertoire {app}/views/plugins/tagging/{controleur}/{action}.ctp.
Si par exemple la Vue qui liste les tags existants ne nous convient pas, nous créons la nôtre dans : {app}/views/plugins/tagging/tags/admin_index.ctp
4. Trouver les Tags
Le Behavior ajoute deux méthodes aux Modèles liés pour manipuler les tags.
findTags($id);
Si l’id du Modèle a déjà été défini, $id est facultatif. Cette méthode retourne les tags associés à cet enregistrement.
findRelated($id, $restrict_to_model, $limit);
Si l’id du Modèle a déjà été défini, $id est facultatif. Si le booléen $restricttomodel vaut true, la méthode retourne les enregistrement de ce Modèle qui ont le plus de tags en commun avec l’enregistrement $id. Si $restricttomodel vaut false, elle retourne les enregistrements tous Modèles confondus qui ont le plus de tags en commun avec l’enregistrement $id.
4.1. Les tags d’un enregistrement
class ArticlesController extends AppController { function view($id) { $this->Article->id = $id; $article = $this->Article->read(); $articleTags = $this->Article->findTags(); $this->set(compact('article', 'articleTags')); } }
Dans la Vue associée :
<h1><?php echo $article['Article']['title']; ?></h1> <?php if(!empty($articleTags)): ?> <p><b>Tags :</b> <?php foreach($articleTags as $tag): ?> <span><?php echo $html->link($tag['Tag']['name'], array( 'plugin' => 'tagging', 'controller' => 'tags', 'action' => 'view', $tag['Tag']['slug'] )); ?></span> <?php endforeach; ?> </p> <?php endif; ?>
4.2. Enregistrements connexes du même Modèle
class ArticlesController extends AppController { function view($id) { $this->Article->id = $id; $article = $this->Article->read(); // Trouve 5 Articles ayant des tags en commun : $relatedArticles = $this->Article->findRelated(true, 5); $this->set(compact('article', 'relatedArticles')); } }
Dans la Vue associée :
<h1><?php echo $article['Article']['title']; ?></h1> <?php if(!empty($relatedArticles)): ?> <h2>Articles en rapport :</h2> <ul> <?php foreach($relatedArticles as $article): ?> <li><?php echo $html->link($article['Article']['title'], array( 'controller' => 'articles', 'action' => 'view', $article['Article']['id'] )); ?></li> <?php endforeach; ?> </ul> <?php endif; ?>
4.3. Enregistrements connexes, tous Modèles confondus
class ArticlesController extends AppController { function view($id) { $this->Article->id = $id; $article = $this->Article->read(); // Trouve 5 ressources ayant des tags en commun : $relatedRessources = $this->Article->findRelated(false, 5); $this->set(compact('article', 'relatedRessources')); } }</code>
Dans la Vue associée :
<h1><?php echo $article['Article']['title']; ?></h1> <?php if(!empty($relatedRessources)): ?> <h2>Ressources en rapport :</h2> <ul> <?php foreach($relatedRessources as $row): $model_name = key($row); switch($model_name) { case 'Article': $link = $html->link($row['Article']['title'], array( 'controller' => 'articles', 'action' => 'view', $row['Article']['id'] )); break; case 'Video': $link = $html->link($row['Video']['title'], array( 'controller' => 'videos', 'action' => 'play', $row['Video']['id'] )); break; } ?> <li><?php echo $link; ?></li> <?php endforeach; ?> </ul> <?php endif; ?>
5. Les Nuages de Tags
5.1. Le nuage de tags d’un Modèle
tagCloud($options);
$options est un tableau associatif d’options :
mincount: nombre minimum de fois qu’un tag a été utilisémaxcount: nombre maximum de fois qu’un tag a été utiliséorder: classement, par ordre alphabétique du nom du tag par défaut : ‘name ASC’limit: nombre maximum de tags retournés
class ArticlesController extends AppController { function index() { $articles = $this->Article->paginate(); $tagCloud = $this->Article->tagCloud(); $this->set(compact('articles', 'tagCloud')); } }
5.2. Le nuage de tags général
Tag::tagCloud($options);
$options est un tableau associatif d’options :
mincount: nombre minimum de fois qu’un tag a été utilisémaxcount: nombre maximum de fois qu’un tag a été utiliséorder: classement, par ordre alphabétique du nom du tag par défaut : ‘name ASC’limit: nombre maximum de tags retournés
class AppController extends Controller { var $uses = array('Tagging.Tag'); function beforeRender() { $mainTagCloud = $this->Tag->tagCloud(); $this->set(compact('mainTagCloud')); } }
5.3. Affichage d’un nuage de tags
5.3.a. Affichage par défaut
echo $tagging->generateCloud($tagCloud);
Produira le code HTML suivant :
<ul> <li><a href="/tagging/tags/view/tag1-slug" class="tag-size-7">Tag 1</a></li> <li><a href="/tagging/tags/view/tag2-slug" class="tag-size-2">Tag 2</a></li> <li><a href="/tagging/tags/view/tag3-slug" class="tag-size-5">Tag 3</a></li> ... </ul>
Notons bien les classes CSS nommées ‘tag-size-x’ (ou x représente l’échelle, comprise entre 1 et 7 par défaut).
5.3.b. Formater les liens du nuage avec un élément
Cette méthode permet d’avoir un contrôle total sur la présentation de chaque tag du nuage. Pour chaque tag, un élément (une sorte de Vue miniature) est appelé.
echo $tagging->generateCloud($tagCloud, array('element' => 'cloud_item'));
L’élement appelé devra se trouver dans {app}/views/elements/clouditem.ctp. Trois variables sont automatiquement disponibles à l’intérieur de cet élément :
$data: les données du Tag$scale: l’échelle comprise entre 1 et$options['maxscale'](ce maximum vaut 7 par défaut)$percentage: échelle exprimée sous forme d’un pourcentage.
<a href="/tagging/tags/view/<?php echo $data['Tag']['id']; ?>" style="font-size:<?php echo $scale; ?>em;"> <?php echo $data['Tag']['name']; ?> </a>
Produira le code HTML suivant :
<ul> <li><a href="/tagging/tags/view/1" style="font-size:7em">Tag 1</a></li> <li><a href="/tagging/tags/view/2" style="font-size:2em">Tag 2</a></li> <li><a href="/tagging/tags/view/3" style="font-size:5em">Tag 3</a></li> ... </ul>
5.3.c. Exemple avec toutes les options
echo $tagging->generateCloud($tagCloud, array( 'max_scale' => 10, // Echelle maximum. Par défaut : 7. 'linkClass' => 'size-class-', // Préfixe du nom de classe CSS affectée à chaque lien de tag. Par défaut : 'tag-size-'. 'element' => false, // Elément, voir paragraphe ci-dessus. Par défaut : false. 'type' => 'div', // Type de l'élément qui va envelopper l'ensemble des tags. Par défaut : 'ul' 'id' => 'tag-cloud', // Identifiant de l'élément 'type' ci-dessus. Par défaut : false. 'class' => 'cloud', // Classe CSS de l'élément 'type' ci-dessus. Par défaut : false. 'itemType' => 'span', // Type de l'élément qui enveloppe un lien de tag. Par défaut : 'li' 'itemClass' => 'cloud-item', // Class CSS de l'élément 'itemType' ci-dessus. Par défaut : false. 'url' => array( // Paramètres de l'URL pour créer les liens vers les tags 'plugin' => null, 'controller' => 'mytags', 'action' => 'read', 'pass' => array('id', 'slug'), 'admin' => false ) ));
Produira le code HTML suivant :
<div id="tag-cloud" class="cloud"> <span class="cloud-item"><a href="/mytags/read/1/tag1-slug" class="size-class-10">Tag 1</a></span> <span class="cloud-item"><a href="/mytags/read/2/tag2-slug" class="size-class-3">Tag 2</a></span> <span class="cloud-item"><a href="/mytags/read/3/tag3-slug" class="size-class-7">Tag 3</a></span> </div>
6. Parcourir les Tags
6.1. Voir un Tag
URL par défaut : /tagging/tags/view/ suivie de l’id du Tag, du slug du Tag (suffixe d’URL utile pour le référencement), ou les deux, peu importe l’ordre.
Nous devons créer une Vue pour cette action, dans {app}/views/plugins/tagging/tags/view.ctp. Les variables suivantes sont disponibles dans la Vue :
$tag: données du Tag.$data: enregistrements paginés associés au Tag.
Exemple de Vue
<?php $this->pageTitle = 'Vue du Tag "' . $tag['Tag']['name'].'"'; $paginator->options(array('url' => $this->passedArgs)); ?> <h1><?php echo $tag['Tag']['name']; ?></h1> <div id="paginator-counter"> <?php echo $paginator->counter(array('format' => "Page %page% sur %pages%, %current% ressources sur %count%")); ?> </div> <?php foreach($data as $row): $model_name = key($row); switch($model_name) { case 'Article': $link = $html->link($row['Article']['title'], array( 'plugin' => null, 'controller' => 'articles', 'action' => 'view', $row['Article']['id'] )); $description = $row['Article']['body']; break; case 'Video': $link = $html->link($row['Video']['title'], array( 'plugin' => null, 'controller' => 'videos', 'action' => 'play', $row['Video']['id'] )); $description = $row['Video']['description']; break; } ?> <div class="ressource"> <h2><?php echo $link; ?></h2> <p align="justify"><?php echo $description; ?></p> </div> <?php endforeach; ?> <div class="paging"> <?php echo $paginator->prev('<< '.__('Précédent', true));?> | <?php echo $paginator->numbers();?> <?php echo $paginator->next(__('Suivant', true).' >>');?> </div>
6.2. Liste des Tags
URL par défaut : /tagging/tags.
Nous devons créer une Vue pour cette action, dans {app}/views/plugins/tagging/tags/index.ctp.
$data: nuage de Tags général.
Exemple de Vue
<?php $this->pageTitle = "Tags"; ?> <h1>Nuage de Tags général</h1> <?php echo $tagging->generateCloud($data, array('id' => 'main-tag-cloud')); ?>
7. Traductions
Par défaut les Vues d’admnistration du plugin sont en anglais.
Langue disponibles :
- Anglais (défaut)
- Français
Pour changer la langue, nous ajoutons la ligne suivante dans notre {app}/config/bootstrap.php :
Configure::write('Config.language', 'fre');
Pour ajouter une traduction, nous pouvons ouvrir le fichier /tagging/locale/tagging.pot avec le logiciel de traduction PoEdit ou un équivalent, puis sauvegarder notre traduction sous forme d’un fichier .po : /tagging/locale/{code de la langue}/LC_MESSAGES/tagging.po.
Toutes les contributions sont les bienvenues, ainsi que les critiques et les commentaires !
Commentaires
30 juin 2009 à 18:51
Salut kalt,
Je viens de tester ton plugin. Je n’ai pas testé toutes ses fonctionnalités pour le moment, mais j’aime beaucoup, je l’adopte.
Des améliorations possibles auxquelles je pense :
$tags = Set::normalize($model->data[$model->alias]['tags'], false, $sep = SEPARATEUR) dans behavior taggable.php L.46 $results[0][$model->alias]['tags'] = join(SEPARATEUR.’ ‘, , Set::extract(‘/Tag/name’, $tags)) dans behavior taggable.php L.93
une gestion des tags via une génération d’une liste de checkbox plutôt qu’un champ texte
un moyen de configurer un autre préfixe d’admin (mais ce n’est peut être pas évident a cause des conventions de nommage des actions controlleur)
Peut être d’autres prochainement lorsque j’aurais joué un peu plus avec.
8 octobre 2009 à 22:30
Bonjour, Merci beaucoup pour cet article très instructif, tout comme les autres d’ailleurs.
Une réaction au paragraphe « 6.1. Voir un Tag »…
Qui mieux que la classe Article sait quel est le texte de description d’un article ? De même pour la description d’une vidéo. Alors pourquoi ne pas ajouter au behaviour Taggable une méthode du genre getDescription(), que redéfinirait chaque classe d’objet « taggable ». Cela rendrait les choses encore un peu plus « élégantes » (avec mille pincettes et grande admiration pour ton code!) ?
Et je serais bien tenté de proposer la même chose pour récupérer le lien HTML de visualisation de l’objet taggué… au risque de malmener un peu la séparation MVC.
Bertrand
6 novembre 2009 à 15:55
Bonjour, merci beaucoup pour tous tes tutoriels, c’est bien utile pour apprendre CakePhp.
En tant que débutant en cake, j’ai une question: Comment puis-je retourner les 5 derniers articles, par exemple, ainsi que les tags associés ?
3 juin 2010 à 22:12
c’est max_scale et non pas maxscale pour le helper $tagging->generateCloud()
26 septembre 2010 à 13:08
i love the plugin.
but a bit slow while using ajax tags input in form.
i am getting weird error/bug when i check request that it sends to « suggest » action in firebug in response tab
« Failed to load source for: http://cakeapp/tagging/tags/suggest«
any idea?
thanks in advance