Add the behavior to the model you want to tag:
$this->addBehavior('Tags.Tag', ['taggedCounter' => false]);
If you want a tag counter in your tagged table, add a migration that adds a tag_count
field into this table.
For now, we skip this.
And in the add/edit forms you can use a basic text input:
echo $this->Form->control('tag_list'); // input e.g.: Foo, Bar, Baz
This will be transformed into the array form automatically on save.
You can even just use the helper:
$this->loadHelper('Tags.Tag');
echo $this->Tag->control();
Your edit action needs to contain the Tags relation to display existing tags into the form:
// Inside get() call in the action
contain: ['Tags'],
This is also important for the patching part to avoid the ORM trying to re-add existing ones.
So a controller "edit" action usually still looks like always:
$article = $this->Article->get($id,
contain: ['Tags'],
);
if ($this->request->is(['patch', 'post', 'put'])) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('Post and its tags has been saved.'));
return $this->redirect(['action' => 'view', $id]);
}
$this->Flash->error(__('The post could not be saved. Please, try again.'));
}
$tags = $this->Articles->Tags->find('list', ['keyField' => 'slug']);
$this->set(compact('article', 'tags'));
Enjoy tagging!
If the default "string" strategy and comma separated text input list does not suffice, you can for example use "array" strategy.
This can be useful when working with select2 and dropdowns (<select>
).
echo $this->Tag->control();
in this case is equivalent with the manual version of
echo $this->Form->control('tag_list', ['type' => 'select', 'multiple' => true, 'options' => ..., 'val' => ...]);
If you need more customization, use the tags
property directly.
When saving the tags, they need to be in the normalized form then on patching.
They can also be combined/stacked with other custom finders, of course.
$taggedRecords = $this->Records->find('tagged', ['tag' => 'tag-slug']);
It also accepts an array here to match any of these tags given.
Note: If you want to use label lookup instead of slug lookup, you need to configure the finderField
.
$untaggedRecords = $this->Records->find('untagged');
You can easily find and display all tags as cloud.
In your controller:
$tags = $this->MyTaggedTable->Tagged->find('cloud')->toArray();
$this->set(compact('tags'));
In your 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.
By default the tags are counted (globally).
You can add the column counter
to the taggable table to also cache this counter for the specific types.
Don't forget to set up some basic validation on your tagged model. You can re-use the same validation if you store it in a more central place.
If you happen to set tags in a non-form context, you can just patch the entity manually:
// $tags could be "Foo, Bar"
$this->Posts->patchEntity($post, ['tag_list' => $tags]);
$this->Posts->saveOrFail($post);
Make sure, that - when updating instead of creating tags - you contained the existing ones in the entity. It should look somewhat like this before patching:
object(App\Model\Entity\Post) {
...
'tags' => [
object(Tags\Model\Entity\Tag) {
'id' => 1,
...
'_joinData' => object(Tags\Model\Entity\Tagged) {
...
}
},
...
]
}
After patching, it should contain the full list of existing/modified ('new' => false
) and to be added entities ('new' => true
) - and should not contain any to be deleted ones.
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, $manager) {
// Here you would have to remap $args if key isn't the expected "tag"
$query->find('tagged', $args);
}
]);
Using orSeparator
/andSeparator
config, one can also filter by multiple tags at once:
// Records that have either "one" or "two" tag
$query->find('tagged', ['slug' => 'one,two']);
// Records that have both "one" and "two" tag
$query->find('tagged', ['slug' => 'one+two']);
For this to be used inside Search plugin, you would usually generate URLs like this:
$this->Html->link('Title', ['?' => ['tag'=> 'one,two']]);
$this->Html->link('Title', ['?' => ['tag'=> 'one+two']]);
Inside the URLs the special chars will automatically be URLencoded.
You can use the untagged finder here inside the search callback.
For this the tag_count
field (and check for 0) is the quickest and easiest. It will otherwise
automatically fallback to a live lookup in the pivot table (tagged).
Your search form might now have an additional value for this in the $tags
array:
$tags['-1'] = '- All without any tags -';
echo $this->Form->control('tag', ['options' => $tags, 'empty' => true]);
Then you just have to switch the query inside the callback in the case of -1
:
'callback' => function (Query $query, array $args, $manager) {
if ($args['tag'] === '-1') {
$query->find('untagged');
} else {
$query->find('tagged', $args);
}
}
To have a behavior attached with different "tags" multiple times, a few config keys have to be overwritten or set.
Let's imagine MultiTagsRecords table and one
, two
tag collections.
$this->addBehavior('TagsOne', [
'className' => 'Tags.Tag',
'fkModelAlias' => 'MultiTagsRecordsOne',
'field' => 'one_list',
'tagsAlias' => 'TagsOne',
'taggedAlias' => 'TaggedOne',
'taggedCounter' => false,
'tagsAssoc' => [
'propertyName' => 'one',
],
'implementedFinders' => [
...
],
'implementedMethods' => [
...
],
]);
$this->addBehavior('TagsTwo', [
'className' => 'Tags.Tag',
'fkModelAlias' => 'MultiTagsRecordsTwo',
'field' => 'two_list',
'tagsAlias' => 'TagsTwo',
'taggedAlias' => 'TaggedTwo',
'taggedCounter' => false,
'tagsAssoc' => [
'propertyName' => 'two',
],
'implementedFinders' => [
...
],
'implementedMethods' => [
...
],
]);
They important config key here is fkModelAlias
which has to be unique per tag collection and therefore per loaded behavior instance.
Also make sure the finders and methods have been aliases to unique ones.
As for your helper call, that one now needs also to be loaded twice. Or you use the inline config:
echo $this->Tag->control(['field' => 'other_list']);
You can set the configuration globally in your app.php using the "Tags" key.
Or you can dynamically set it on each addBehavior()
method call as well as when loading the helper.
The most important ones are:
'taggedCounter'
: Set to false if you don't need a counter cache field in your tagged table.'strategy'
:'string'
/'array'
'delimiter'
- Separating the tags in input elements or as string list, e.g.:','
'namespace'
-'string'
to use for internal namespace column. Do not use together with separator if you want to keep it internal. Otherwise it will become the default namespace and visible.'separator'
: For namespace prefix, e.g.:':'
. With this set the namespace will be parsed from the tag.'andSeparator'
- Allows AND filtering, e.g.:'+'
or'&'
'orSeparator'
- Allows OR filtering, e.g.:','
or'|'
You can set them globally using Configure and the Tags
config key.
If you need also to pass options to the slug behavior, use an array config for it:
'slugBehavior' => [
'Tools.Slugged' => [
'mode' => [Text::class, 'slug'],
...
],
If you want to use custom slugging, use the 'slug'
callable you can provide to the behavior:
'slug' => function($tag) {
return (new CustomSlugger())->slug($tag);
}
By default, the plugin works with AIIDs (auto-incremental IDs). This usually suffices, as the tags are usually not exposes via ID, but via slug.
As such the internal ID is usually not leaking to the outside.
If you, for some reason, still need to use UUIDs, please copy over the schema to your project's /config/Migrations/
folder and adjust the primary key in the migration files to 'type' => 'uuid', 'length' => 36, 'null' => false
.
Make sure you didn't add any validation like "numeric" here, only "scalar" ideally. See the test cases (and fixtures for UUIDs) for details.
If you would like to tag (at least) one model that uses an UUID as a primary key, you will need to adjust the migration and change the foreign key field fk_id
from integer
to uuid
as well.
This will work with UUIDs as well as with AIIDs.
If you create your own APP Tags controller, you can easily have EntityRouting set up for it:
$routes->connect('/tag/:slug', ['controller' => 'Tags', 'action' => 'view'], ['routeClass' => 'EntityRoute']);
In your templates you can then build URLs with the entities passed along directly:
echo $this->Html->link($tag->label,
[
'controller' => 'Tags',
'action' => 'view',
'_entity' => $tag,
]
);
Defining a route name you could even just use the short form '_name' => 'my-tag-alias', '_entity' => $tag
for the links:
$routes->get('/tag/:slug', ['controller' => 'Tags', 'action' => 'view'], 'my-tag-alias');
For details see Core docs.
For higher productivity use the IdeHelper plugin to auto-add the annotations for your new relations.
This will most likely add the following annotations to your table class:
* @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
And also some in your entity:
* @property \Tags\Model\Entity\Tagged[] $tagged
* @property \Tags\Model\Entity\Tag[] $tags
For helper usage in the templates this will be added to AppView:
* @property \Tags\View\Helper\TagHelper $Tag
These will help you, your IDE and tooling like PHPStan to understand the relations and how to use them. The IdeHelper will also give you autocomplete on those for all loadModel() calls as well as autocomplete on the custom finders.
The only manual annotation you will have to add, is the tag_list
for the entity:
* @property string $tag_list !
You do not necessarily need to have:
protected $_accessible = [
'*' => true,
'id' => false,
];
The TagsBehavior will usually automatically make the needed tags
field accessible for patching.
If in doubt or if patching doesn't work as expected, double check if those fields have been properly made accessible.
Only if you need to store more data than the default fields, you might have to additionally whitelist those, as well.