Multi-model Tagging plugin for CakePHP

This plugin provides an easy to install solution for tagging any Model of your application.

Features :

  • Easy install : download the files and create 2 db tables, no matter how many Models you want to tag.
  • To tag a Model, add an input field in your forms : a simple text input or an advanced one with existing tags suggested as you type.
  • Includes a Behavior that adds methods to linked Models to find record’s tags, related records that share the most tags, etc.
  • Build and display tag clouds, with a total control of the tags you want to retrieve and how you want to display them.

1. Installation

1.1 Files

To install, copy the ‘tagging’ directory to the ‘plugins’ folder:

git clone git://github.com/kalt/tagging.git

Or click ‘download’ and copy the content of the zip file into your ‘plugins’ folder.

1.2. DB Tables

Create two db tables as described in tagging/config/sql/tagging.sql

Or run this in console:

cake schema run create Tagging -path plugins/tagging/config/sql

2. Basic setup

Let’s see how to tag any Model. The plugin provides a Behavior and a Helper to achieve that.

2.1. Models

class Article extends AppModel
{
	var $actsAs = array('Tagging.Taggable');
}

2.2. Controllers

class ArticlesController extends AppController
{
	var $helpers = array('Tagging.Tagging');
}

2.3. Views

A single input field for comma-separated tags, in add/edit views.

2.3.a. Classic input

echo $form->input('tags');

2.3.b. With ajax tag suggestions

echo $tagging->input('tags');

Requires jQuery in <head>...</head>

That’s it, you can tag any Model.

2.3.c. Ajax tag suggestions options

If you choose this solution to add tags to a record, you will see suggested tags as you type.

You can change any option as with the core Paginator Helper:

$tagging->options(array('option_key' => 'value', ...));

Here are the default options :

  • selector : DOM selector to be observed (try to keep it simple, one id (‘#xyz’) or one class (‘.xyz’) only). Defaults to ‘.tagSuggest’.
  • url : url to get suggestions via ajax POST call (JSON formatted response). Defaults to ‘/tagging/tags/suggest’.
  • delay : sets the delay between keyup and the request (in milliseconds). Defaults to 500.
  • start : minimum length of the word before a request is sent. Defaults to 1.
  • limit : maximum number of results. Defaults to 10.
  • matchClass : CSS class applied to the suggestions. Defaults to ‘tagMatches’.
  • sort : boolean to force the sorted order of suggestions. Defaults to false.
  • tagContainer : the type of element used to contain the suggestions. Defaults to ‘span’.
  • tagWrap : the type of element the suggestions a wrapped in. Defaults to ‘span’.
  • tags : array of tags specific to this instance of element matches (if you don’t want to use ajax but a predefined list of tags). Defaults to null.

If you want to change the way tag suggestions are displayed, simply change the ‘matchClass’ option and provide your own CSS styles according to the new ‘matchClass’ name.

The second parameter of $tagging→input() can be an array of options just as the classic $form→input().

3. Managing Tags

Enable admin routing in {app}/config/core.pp

Go to URL /admin/tagging: you can add a tag or view existing tags.

The default views here are very basic, they just had been baked with the cake “bake” shell.

If any of the default views in this plugin does not meet your needs, create your own and place it in your own app folder, in {app}/views/plugins/tagging/{controller name}/{view name}.ctp

For example, if you want to customize the admin list of tags, simply create your own in: {app}/views/plugins/tagging/tags/admin_index.ctp

4. Finding Tags

findTags($id);

If Model id is set, $id is optionnal. Returns record’s tags.

findRelated($id, $restrict_to_model, $limit);

If Model id is set, $id is optionnal. If $restrict_to_model is true, returns model’s records that share the most tags with record of id $id. If $restrict_to_model is false, returns all records that share the most tags with record of id $id.

4.1. Record’s tags

class ArticlesController extends AppController
{
	function view($id)
	{
		$this->Article->id = $id;
		
		$article = $this->Article->read();
		
		$articleTags = $this->Article->findTags();
		
		$this->set(compact('article', 'articleTags')); 
	}
}

In the corresponding view :

<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; ?>

<?php endif; ?>

4.2. Record’s related records (same Model)

class ArticlesController extends AppController
{
	function view($id)
	{
		$this->Article->id = $id;
		
		$article = $this->Article->read();
		
		// Find 5 related Articles :
		$relatedArticles = $this->Article->findRelated(true, 5);
		
		$this->set(compact('article', 'relatedArticles')); 
	}
}

In the corresponding view :

<h1><?php echo $article['Article']['title']; ?></h1>
<?php if(!empty($relatedArticles)): ?>
<h2>Related Articles:</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; ?>

<?php endif; ?>

4.3. Record’s related records (all Models)

class ArticlesController extends AppController
{
	function view($id)
	{
		$this->Article->id = $id;
		
		$article = $this->Article->read();
		
		// Find 5 related Ressources :
		$relatedRessources = $this->Article->findRelated(false, 5);
		
		$this->set(compact('article', 'relatedRessources')); 
	}
}

In the corresponding view :

<h1><?php echo $article['Article']['title']; ?></h1>
<?php if(!empty($relatedRessources)): ?>
<h2>Related Ressources:</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;
} ?>

  • <?php echo $link; ?>

  • <?php endforeach; ?>

    <?php endif; ?>

    5. Tag Clouds

    5.1. Tag Cloud (Model specific)

    tagCloud($options);

    $options is an array of options :

    • min_count : minimum number of times a tag is used
    • max_count : maximum number of times a tag is used
    • order : tags order, defaults to ‘name ASC
    • limit : number of tags returned
    class ArticlesController extends AppController
    {
    	function index()
    	{
    		$articles  = $this->Article->paginate();
    		$tagCloud = $this->Article->tagCloud();
    		
    		$this->set(compact('articles', 'tagCloud')); 
    	}
    }

    5.2. Tag Cloud (all Models)

    Tag::tagCloud($options);

    $options is an array of options :

    • min_count : minimum number of times a tag is used
    • max_count : maximum number of times a tag is used
    • order : tags order, defaults to ‘name ASC
    • limit : number of tags returned
    class AppController extends Controller
    {
    	var $uses = array('Tagging.Tag');
    
    	function beforeRender()
    	{
    		$mainTagCloud = $this->Tag->tagCloud();
    		
    		$this->set(compact('mainTagCloud')); 
    	}
    }

    5.3. Displaying tag clouds

    5.3.a. Basic display

    echo $tagging->generateCloud($tagCloud);

    Will output:

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

    Note the ‘tag-size-x’ CSS class (with x, the ‘scale factor’, between 1 and 7 by default).

    5.3.b. Format cloud items with an element

    echo $tagging->generateCloud($tagCloud, array('element' => 'cloud_item'));

    Element in views/elements/cloud_item.ctp:
    3 predefined values available :

    • $data: tag array
    • $scale: scale factor, from 1 to $options[‘max_scale’] (defaults to 7)
    • $percentage: scale factor from 0 to 100.
    <a href="/tagging/tags/view/<?php echo $data['Tag']['id']; ?>" style="font-size:<?php echo $scale; ?>em;">
    	<?php echo $data['Tag']['name']; ?>
    </a>

    Will output:

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

    5.3.c. All output options

    echo $tagging->generateCloud($tagCloud, array(
    	'max_scale' => 10,            // CSS class max scale
    	'linkClass' => 'size-class-', // CSS class name prefix
    	'element' => false,           // Element, see above
    	'type' => 'div',              // Type of output 
    	'id' => 'tag-cloud',          // DOM id for top level 'type'
    	'class' => 'cloud',           // CSS class for top level 'type'
    	'itemType' => 'span',         // type of item output
    	'itemClass' => 'cloud-item',  // CSS class for items of type 'itemType'
    	'url' => array(               // URL params
    		'plugin' => null,
    		'controller' => 'mytags',
    		'action' => 'read',
    		'pass' => array('id', 'slug'),
    		'admin' => false
    	)
    ));

    Will output:

    <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. Browsing Tags

    6.1. View tag

    Default URL: /tagging/tags/view/ followed by tag id, tag slug, or both in any order.

    You have to create a view for this action, in {app}/views/plugins/tagging/tags/view.ctp

    • Tag data available in $tag.
    • Paginated tagged records data available in $data.

    Exemple

    <?php
    $this->pageTitle = 'Viewing 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% on %pages%, %current% ressources on %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('<< '.__('Previous', true));?>
     |  <?php echo $paginator->numbers();?>
    	<?php echo $paginator->next(__('Next', true).' >>');?>
    </div>

    6.2. List tags

    Default URL: /tagging/tags

    You have to create a view for this action, in {app}/views/plugins/tagging/tags/index.ctp

    Tag cloud data available in $data.

    Exemple

    <?php $this->pageTitle = "Tags"; ?>
    
    <h1>Main Tag Cloud</h1>
    
    <?php echo $tagging->generateCloud($data, array('id' => 'main-tag-cloud')); ?>

    7. Translations

    Available languages :

    • English (default)
    • French

    To change (for French for example), add this line in your app/config/bootstrap.php :

    Configure::write('Config.language', 'fre');

    To add a translation, open the file /tagging/locale/tagging.pot in PoEdit or equivalent, then save the translation to /tagging/locale/{your language}/LC_MESSAGES/tagging.po