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 !

Pierre-Emmanuel Fringant

Articles connexes

Commentaires

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 :

  • possibilité de configurer le séparateur de tags directement via la déclaration actsAs (actuellement ‘,’) :

$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.

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

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 ?

c’est max_scale et non pas maxscale pour le helper $tagging->generateCloud()

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

Bonjour,

Merci pour ce plugin, je viens de trouver le site et c’est une belle petite mine d’or :)

Malheureusement, j’utilise la version 2.2…

Je ne suis pas assez a l’aise avec cakephp pour faire les modifs

y a t’il une version pour la 2.2 ?

Merci de votre aide seb

Participez

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