Developers
Tags
Tags are free text a member attaches to something — a resource, a wiki page, a booking, another member. Any component can offer them, and they all share one vocabulary, so a tag applied to a resource and the same tag applied to a group are the same tag and lead to the same page.
You do not build this yourself. Adding tags to a component is one small class and two calls:
// core/components/com_bookings/models/tags.php
namespace Components\Bookings\Models;
class Tags extends \Components\Tags\Models\Cloud
{
protected $_scope = 'bookings';
}
// saving
$cloud = new Tags($instrument->get('id'));
$cloud->setTags(Request::getString('tags', ''), User::get('id'));
// displaying
echo $cloud->render();
Everything else on this page is detail around those two calls.
How they are stored
Two tables. #__tags holds the vocabulary, one row per distinct tag:
| Column | Notes |
|---|---|
id |
|
tag |
The normalised form — lowercase, alphanumerics only. Unique |
raw_tag |
What the member typed, including case and spacing |
description |
Optional, edited by an administrator |
admin |
0 member tag, 1 administrator tag, 2 core tag |
created, created_by, modified, modified_by |
#__tags_object records each attachment:
| Column | Notes |
|---|---|
tagid |
The tag |
objectid |
The id of the thing tagged |
tbl |
The scope — which component the object belongs to |
taggerid |
Who applied it |
taggedon |
When |
strength |
Weighting, 1 by default |
label |
An optional sub-category within the scope |
tbl is what keeps a blog entry with id 77 apart from an event with id 77.
It is the reason every component that tags things declares a scope of its
own.
Because the vocabulary is shared, renaming or merging a tag in the administrator's Tags screen changes it everywhere at once.
Declaring a scope
Subclass Components\Tags\Models\Cloud
and set $_scope. The whole class is that:
namespace Components\Blog\Models;
use Components\Tags\Models\Cloud;
require_once \Component::path('com_tags') . DS . 'models' . DS . 'cloud.php';
/**
* Blog Tagging class
*/
class Tags extends Cloud
{
/**
* Object type, used for linking objects (such as resources) to tags
*
* @var string
*/
protected $_scope = 'blog';
}
Pick a scope name once and never change it. #__tags_object rows are found
by tbl, so renaming a scope does not migrate anything: every existing tag
on every existing record silently stops being found, and the records look as
though they were never tagged. Nothing errors.
Do not reuse another component's scope either. Two components sharing a scope means record 12 in yours and record 12 in theirs carry each other's tags.
Every component in the tree that tags anything has a models/tags.php of
this shape.
Reading and writing
Construct it with the id of the object being tagged:
use Components\Bookings\Models\Tags;
$cloud = new Tags($instrument->get('id'));
Cloud::__construct($scope_id = 0, $scope = '') also lets you pass a scope
directly if you would rather not subclass, and
Cloud::getInstance($scope_id, $scope) returns a per-request cached one.
Setting the tags on an object is a single call, and it is the same call whether the object had tags before or not:
$tags = Request::getString('tags', '');
$cloud = new Tags($instrument->get('id'));
if (!$cloud->setTags($tags, User::get('id')))
{
$this->setError($cloud->getError());
}
That guard is what every controller in the tree writes, and it is worth knowing that it never fires — see the warning below.
setTags($tag_string, $tagger_id = 0, $admin = 0, $strength = 1, $label = '') takes a comma-separated string, works out which tags are new and which
have gone, adds and removes accordingly, and leaves untouched tags alone so
their timestamps survive. With $tagger_id left at 0 it uses the current
user.
| Method | What it does |
|---|---|
setTags($string, $tagger, $admin, $strength, $label) |
Replace the object's tags with this list |
add($tags, $tagger, $admin, $strength, $label) |
Add without removing anything |
remove($tags, $tagger) |
Remove the named tags |
removeAll($tagger) |
Remove every tag on the object |
tags($rtrn, $filters, $clear) |
The tag rows themselves |
render($rtrn, $filters, $clear) |
Formatted output — see below |
normalize($tag) |
The normalised form of one raw tag |
add() creates any tag that is not in the vocabulary yet, then links it.
add(), remove() and removeAll() return false and set an error when
the cloud has no scope_id, so construct it with an id.
Rendering
$cloud = new Tags($entry->get('id'));
echo $cloud->render();
| Argument | Returns |
|---|---|
render() or render('html') or render('cloud') |
An <ol class="tags"> of linked <li> items, rendered from com_tags' _cloud layout |
render('string') |
The raw tags, comma-separated: My Tag, Your Tag |
render('array') |
A flat array of the normalised tag strings: ['mytag', 'yourtag'] |
The HTML form comes from com_tags' _cloud layout, which reads
show_sizes, show_tag_count and show_tags_sort from
Component::params('com_tags') to decide between a plain alphabetical list,
a weighted cloud, and a list with counts. Note that none of those three is
declared in com_tags/config/config.xml, so they take their defaults — a
plain alphabetical list — unless a hub sets them another way.
render('string') is what an edit form wants, because it round-trips
through setTags():
$tags = $cloud->render('string');
Do not use render('array') for that. It returns the tag column — the
normalised form — so the edit form redisplays the member's Mass
Spectrometer as massspectrometer. render('string') returns raw_tag,
which is what they typed.
For the tag rows themselves — id, raw_tag, admin, count — call
tags('list') and iterate.
The tag editor
The autocompleting input every edit form uses comes from the
hubzero/autocompleter plugin, reached by event:
$tf = Event::trigger('hubzero.onGetMultiEntry', array(
array('tags', 'tags', 'actags', '', $this->escape($tags))
));
if (count($tf))
{
echo $tf[0];
}
else
{
echo '<input type="text" name="tags" value="' . $this->escape($tags) . '" />';
}
The inner array is positional: what to complete (tags, members,
groups), the input's name, its id, a CSS class, and the current value
as a comma-separated string. The fallback matters — the plugin can be
disabled, and trigger() then returns an empty array.
hubzero.onGetSingleEntry is the same thing for a field that takes one
value.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.