Tutorial: CakePHP & Tagging

This article mainly shows the power of a Rapid Development Framework like CakePHP.
The full plugin documentation can be found directly in the repository.

Let’s imagine we have some blog posts in our database and we need to quickly add tagging functionality.
So let’s make our backend support adding and editing tags for a post, then display them in the paginated index and also in the public view.

Setup

We first composer require the plugin:

composer require dereuromark/cakephp-tags

We then need to load the plugin. One can just use CLI here too:

bin/cake plugin load Tags

Then we need to add the default Tags plugin tables using Migrations plugin:

bin/cake migrations migrate -p Tags

I usually add this into the composer script section:

"scripts": {
    ...
    "migrate": [
        "bin/cake migrations migrate -p Queue",
        "bin/cake migrations migrate -p Tags", // Added
        "bin/cake migrations migrate"
    ]
},

So I only have to execute one command locally and for deployment:

composer migrate

Our PostsTable class needs to know about the tagging functionality now:

	public function initialize(array $config) {
        parent::initialize($config);

        ...
        $this->addBehavior('Tags.Tag', ['taggedCounter' => false]);
    }

We don’t add an app migration for a counter cache field for now, even though this can easily be done if needed.

We load the helper in the AppView class:

	public function initialize() {
        ...
        $this->loadHelper('Tags.Tag');
    }

Let’s adjust the forms now:

// Adding the input field into the add and edit form
    echo $this->Tag->control();

Finally, our edit action needs to know that it should also fetch already saved tags to pre-populate the input field:

// Inside edit action
$post = $this->Posts->get($id, [
    'contain' => ['Tags'] // Add this line
];

That should be it. If you open up the add action, we can add comma separated tags like Foo, Bar and they would be displayed when you open up the edit action. You can modify and see that this also works already.

Only very few lines and a few configuration flags and you got a whole bunch of functionality out of the box for your application.
Time so far: 10 minutes.

Displaying

Now let’s add a nice list in both index and view:

// In the index action
$this->paginate['contain'] = ['Tags'];

// In the view action
'contain' => ['Tags'] // Add this line

// In the templates
echo h($post->tag_list) // Outputs a comma separated list

If you want more control, you can use the tags array:

// Either as helper
echo $this->SomeHelper->displayTags($post->tags);

// Or as element
echo $this->element('tags', ['tags' => $post->tags]);

Available fields per tag: id, namespace, slug, label, counter, created, modified, …

You could also create links to the index that allows filtering by tag:

echo $this->Html->link($tagName, ['action' => 'index', '?' => ['tag' => $tagSlug]]);

This would work together nicely with e.g. friendsofcake/search plugin (see tips secion).

Time so far: 20 minutes.

Tag Cloud

Wouldn’t it be only a half-solution if we couldn’t display the tags in a nice way, showing
the most used ones on some tags dashboard URL? 🙂

In our controller we can add a tags action:

$tags = $this->Posts->Tagged->find('cloud')->toArray();
$this->set(compact('tags'));

In the tags.ctp template:

<?php
$this->loadHelper('Tags.TagCloud');

echo $this->TagCloud->display($tags, ['shuffle' => false], ['class' => 'tag-cloud']);
?>

With a bit of custom CSS you can make each tag a floating element.
By default the shuffle is enabled, you can disable using 'shuffle' => false config as shown above.

Time total: 30 minutes max.

You can see that adding functionality like this can be hooked in rather easily in such a convention driven framework.
Check out the awesome list for a few more of those.

Tips

Security

This tutorial was mainly for a CRUD kind of backend application.
If you expose the functionality – maybe even with some AJAX – to the frontend, make sure that Csrf/Security components, as well as field whitelisting, are definitely in place (incl. $accessible fields of the entity).

If users can modify their posts’ tags, make sure that the id of the user matches the user_id of the post before allowing any modification.
Add, Edit, Delete actions should always be non-GET. $this->request->allowMethod('post') etc can help here.
The default bake templates can guide you here. They come with good defaults out of the box.

Note that all output of tags is always secured using h(), especially when other users can enter/add tags. This way you prevent XSS vulnerabilities.

Combining it with filtering

You can easily combine the tagged custom finder with e.g. Search plugin.
This way you can add a filter to your paginated index action.

Just pass a list of tags ([slug => name] pairs) down to the view layer where you populate the search form field as dropdown, for example:

echo $this->Form->control('tag', ['options' => $tags, 'empty' => true]);

In your table’s searchManager() configuration you will need a small callback config:

$searchManager
    ...
    ->callback('tag', [
        'callback' => function (Query $query, array $args) {
            // Here you would have to remap $args if key isn't the expected "tag"
            $query->find('tagged', $args);
        }
    ]);

The CakePHP ORM here is really powerful and will automatically use inner joins here for the query.
An example of the generated SQL can be seen here.

Adding an untagged filter

In some cases you want to allow filtering for all records without tags. In this case the find('untagged') custom finder can come in handy:

		'callback' => function (Query $query, array $args, $manager) {
            if ($args['tag'] === '-1') {
                $query->find('untagged');
            } else {
                $query->find('tagged', $args);
            }
        }

Your form then just needs an extra row here for the dropdown:

$tags['-1'] = '- All without any tags -';
echo $this->Form->control('tag', ['options' => $tags, 'empty' => true]);

Auto-complete and IDE usability

You will notice that in the templates, for example, you can’t click on the Tags::control() method just yet.
The new properties of the entity or the behavior’s methods in our table are also yet unknown to the IDE.

Run the IdeHelper to auto-add the missing annotations into your classes:

bin/cake annotations models -v
bin/cake annotations view -v

It will add

// For the table
@property \Tags\Model\Table\TaggedTable|\Cake\ORM\Association\HasMany $Tagged
@property \Tags\Model\Table\TagsTable|\Cake\ORM\Association\BelongsToMany $Tags
@mixin \Tags\Model\Behavior\TagBehavior

// For the entity
@property \Tags\Model\Entity\Tagged[] $tagged
@property \Tags\Model\Entity\Tag[] $tags

// For the AppView and thus the templates
@property \Tags\View\Helper\TagHelper $Tag

The only one that you have to add manually:

// For the entity
@property string $tag_list !

since this cannot be known by the annotations shell.

Tags backend

The plugin itself does not ship with controllers or routing. You can easily add this yourself in your application, though.
Just bake the Tags controller with some CRUD actions for your backend (e.g. using --prefix admin).

More advanced usage and configuration

Auto-complete, validation, colors, scopes (namespaces), AJAX, counters and counter caches, …: See the plugin directly.

Whatever is missing: Feel free to PR (pull request) enhancements here.

Demo

This article and its demo were written using CakePHP 3.6.
A live demo and some showcases can be found in the sandbox.

3.86 avg. rating (78% score) - 7 votes

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.