Tags API Guide

From Joomla! Documentation

Introduction

This is one of a series of API Guides, which aim to help you understand how to use the Joomla APIs through providing detailed explanations and sample code which you can easily install and run.

This guide covers the APIs related to Joomla Tags functionality. Most of the APIs relevant to tags are found in the TagsHelper class, but this guide covers only a subset of these functions, as many of them are designed for Joomla core use only, and not well suited to use by an externally-developed extension.

Tags Table

Tags are held in the tags table using the Nested Set Model, which means that they can follow a tree hierarchy structure. You can access the data using the Joomla Table class APIs, and also the APIs which related to Nested Set Tables. These APIs are described in Table Basic API Guide and Using nested sets.

To get a Table instance do:

use Joomla\CMS\Table\Table;
$tagTable  = Table::getInstance('Tag', 'TagsTable');

You can then access the records and fields as described in, for example, Table Basic API Guide. This allows you also to update and delete Tag records, but be aware that if you delete tag records this way, then any entries in the contentitem_tag_map table described below will not be removed.

There are also 3 TagsHelper functions which provide access to tag data.

getTagNames() – you provide an array of tag ids and get returned an array of the corresponding tag titles.

$th = new TagsHelper();
$tagnames = $th->getTagNames(array('2', '3'));  // returns something like array('tag2', 'tag3')

getTagTreeArray() – you provide the id of the tag and get returned an array containing the id you passed in, plus the ids of the children in the tree hierarchy. Tag ids are returned as strings.

$th = new TagsHelper();
$result = $th->getTagTreeArray($tagid);

searchTags() – you provide an associative array of filters and you get back an array of the tags which match the filters. The filters include language (specified as "flanguage"), title/path (specified as "like"), published state (specified as "published"), access (specified as "access"). To see the full list look at the source of the function in the TagsHelper class file.

What is returned is an array of tags which match the search conditions, with each tag having properties which are:

  1. value – id of the tag record
  2. path – the series of tag aliases from the root of the tag tree (but excluding the root itself) down the tree hierarchy to this tag (rather like a file path).
  3. text – the same as the path, but with each alias replaced by the corresponding tag title.
$filters = array('like' => "tag");
$results = TagsHelper::searchTags($filters);  // note that this is a static function
foreach ($results as $result)
{
	echo "Id: " . $result->value . ", Title Path: " . $result->text . ", Path: " . $result->path;
} 	
// outputs something like 
// "Id: 1, Title Path: My Tag, Path: my-tag"
// "Id: 2, Title Path: My Tag/Child of My Tag, Path: my-tag/my-child-tag"

Tags – Content Database Structures

The relationship between Tag and the records of Joomla items such as articles, contacts, etc is a many-many relationship, hence this is resolved through the use of a join table contentitem_tag_map, which contains:

  1. a tag_id, which points to the tag record
  2. a type_alias and a content_item_id which together point to the item which has that tag.

The type_alias is of the form "com_content.article", "com_content.category", "com_contact.contact", etc and defines the type of item, and hence the database table to find that item in.

The content_item_id is the id of the record in that item table.

Joomla Tags and UCM

There's not a lot more necessary to implement tags functionality, but the Joomla implementation seems to have been done when a number of Joomla developers believed that the best way to evolve Joomla was to move to having data items in a "Unified Content Model" arrangement. This involved holding the data that was common to items such as articles, categories, banners in a common ucm_content table rather than being spread around the individual content, categories, banners, etc tables. Although this approach seems to be now abandoned, it has left a legacy of complex structures related to tags, as shown in the diagram.

When a tag is added to an item, for example when an administrator edits an item in the back-end, then when the record is saved to the database it's checked to see if there are associated tags, and if so then two ucm records (one ucm_base and one ucm_content) are written to the database.

Into the ucm_content record are written fields which are common to several Joomla component tables, such as title, description, alias, creation date/time/user, checkout data, params, etc. The fields are mapped from the source table columns using the data which is specified in the content_types table, which specifies the columns in the source table where ucm should look to find the title, description, alias, etc data.

Once the ucm_content record is created, a ucm_base record is created which contains:

  1. the id of the ucm_content record just created
  2. the id of the content_types record, specifying the type of Joomla component data
  3. the id of the source item, in the Joomla component table
  4. the id of the language associated with the source item.

The id of the ucm_content record is also written to the contentitem_tag_map record.

Two things are important to note!

  • The various column names used to identify the id of the ucm record and the id of the source table record – eg core_content_id, content_item_id, core_content_item_id etc – can be confusing. If you're using them then make sure you know which is which!
  • Although the ucm records refer to "content_item" etc, this doesn't just refer to what is commonly called "content" within Joomla, namely com_content and articles. Rather it includes also other items such as contacts, users and related categories, and you can see a full list by examining the records in the content_types table.

If you develop a Joomla component and want to enable tags functionality for that component, then one thing you will need to do is to build the content_types data to supply the mapping between your component table columns and the fields of the ucm_content record. You can read further details about this in J3.x:Using Tags in an Extension and there is a worked example in J3.x:Developing an MVC Component/Adding Tags

Tag – Content Links

You can get the tags associated with an individual item through the following 2 functions:

getTagIds() – you pass in the component and id/ids of the component items and it returns you a string of the ids of the tags associated with this item / these items, getting the data from contentitem_tag_map. For example:

$th = new TagsHelper();
$result = $th->getTagIds(23, "com_content.article");   
// returns the tag ids of tags associated with article 23 eg '5,6,8'

This function is often used to obtain the current tag ids associated with an item so that they can be rendered in a Tag form field type field for editing purposes.

getItemTags() – this is similar to getTagIds() in that it gets the tags associated with the item you pass in, but it also allows you to get all the values of the fields in the tags table for those records. (Note that the parameters are the other way round from getTagIds()).

$th = new TagsHelper();
$result = $th->getItemTags("com_content.article", 23); 
// gets all the tag fields of the tags which are associated with article 23

This function is often used on the site front-end when displaying the tags associated with an item.

Conversely, you can find the items which are associated with a given tag (or array of tags) using getTagItemsQuery(). This returns a query that you can run to get details of the items which have the passed-in tag id or ids.

$th = new TagsHelper();
$query = $th->getTagItemsQuery($tagid);  // query for content which has the tag with id = $tagid
$db = Factory::getDbo();
$db->setQuery($query);
$results = $db->loadAssocList();
foreach ($results as $result)
{
	echo "<br>Type alias: " . $result["type_alias"] . ",Id: " . $result["content_item_id"] . ",Title: " . $result["core_title"];
}

Apart from the type_alias, which indicates the type of item associated with a tag, the fields returned are all fields from the ucm_content record rather than from the original source record. So they are named as in the columns of the ucm_content table, as indicated above.

If you configure a front-end menuitem which has a type of "Tagged Items" then getTagItemsQuery() is used there to obtain the set of items of various types which have the selected tag or tags.

Associating Tags with Items

To associate a tag with an item you should use the Joomla Table functionality for the item's table. Set the Table instance property newTags to be an array of the ids of the tags you want that item to have. For example:

$table = Table::getInstance('Content');
$table->load("2");   // load article which has id=2
$table->newTags = array("1", "4");
$table->store();

The above will tag article 2 with tags 1 and 4.

If your model inherits from the AdminModel, then when you do

$model->save($data)

the AdminModel code sets the newTags property for you in the line

$table->newTags = $data['tags'];

The Tags Observer which listens for updates to the content table will examine the newTags property and will create the appropriate records in the contentitem_tag_map table and create the ucm base and content records if necessary.

You can also arrange for new tags to be created on the fly, by preceding the name of the new tag by "#new#". For example,

$table->newTags = array("1", "#new#another tag", "4");

will result in the tag with title "another tag" being created in the tags table as well.

To update the tags associated with an item, you just specify new set of tags in the newTags property and proceed as above. The Tags Observer will look after removing the records of the old tags in the contentitem_tag_map table, as well as creating the new links.

Important! If you're updating an item with tags via the Table functionality, and you don't specify the newTags property, then all the existing tag associations for that item will be removed! To maintain them do:

$th = new TagsHelper();
$currentTags = $th->getTagIds("1", "com_content.article"); // insert your item here
$table->newTags = explode(',',$currentTags); 
// before you call
$table->store();  // or $table->save();

If you have developed your own component, and it follows the Joomla pattern of having a tags field (named "tags") present in the form for editing an item, and also using a model which inherits from AdminModel, then the Joomla core code will take care of setting the newTags Table property and ensuring your tags are not lost.

Removing Tag Associations with Items

To remove a tag from being associated with an item, you can use the Table functionality, specifying the revised set of tags in the newTags property, omitting the one you wish to disassociate.

If you want to remove all tags from an item, then you can omit the newTags property completely, or set it to null, and then call $table->store().

Alternatively you can use the deleteTagData($table, $contentItemId) function, however this requires you to know the id of the ucm_content record. This function isn't generally used in Joomla outside the Tags Observer/Helper classes.

Sample Component Code

Below is the code for a small component which you can install and run, and can adapt to experiment further with tags APIs. Create the following 3 files in a folder called com_sample_tags, then zip up this folder to create com_sample_tags.zip, and install this component on your Joomla instance. Once installed, navigate on your browser to your Joomla site, and add to the URL the parameter &option=com_sample_tags.

The component demonstrates some of the APIs described above which provide access to the tags data. It also presents a form which you can use to edit the tags associated with the article which has id=1. (You can edit the code to change this id, if you prefer).

(As described in Basic form guide, the approach below isn't the recommended way to design Joomla components, but it's written in this minimalistic fashion to focus on the tags API aspects).

com_sample_tags.xml Manifest file for the component

<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.1.0" method="upgrade">

	<name>com_sample_tags</name>
	<version>1.0.0</version>
	<description>Sample tags component</description>
	
	<administration>
	</administration>

	<files folder="site">
		<filename>sample_tags.php</filename>
        <filename>tags_form.xml</filename>
	</files>
</extension>

sample_tags.php The main code file which is run when an HTTP GET or POST is directed towards this component.

<?php
defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Form\Form;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Helper\TagsHelper;

$tagsHelper = new TagsHelper();
$article = Table::getInstance('Content');
$form = Form::getInstance("sample", __DIR__ . "/tags_form.xml", array("control" => "myform"));

if ($_SERVER['REQUEST_METHOD'] === 'GET') 
{
	// get a test article
	$article->load(1);   // change to another id if you like
	// get its current tags
	$currentTags = $tagsHelper->getTagIds($article->id, "com_content.article");
	// display names of article's current tags
	$currentTagnames = $tagsHelper->getTagNames(explode(',', $currentTags));
	echo "Article has tags: " . implode(',', $currentTagnames);
}
else // POST request
{
	$app   = Factory::getApplication();
	$data = $app->input->post->get('myform', array(), "array");
	// update article in database with data from form
	$article->load($data['id']);
	$article->bind($data);
	$article->newTags = $data["tags"];  // commenting out this line will result in tags being removed from the article
	$article->check();
	$article->store();
	// get data for redisplaying form, based on what's now in the database
	$article->load($data['id'], true);
	$currentTags = $tagsHelper->getTagIds($article->id, "com_content.article");
}
// set up data for the form
$prefillData = array('id'=>$article->id, 'title'=>$article->title, 'alias'=>$article->alias, 'tags'=>$currentTags);
$form->bind($prefillData);
// find other items with any of the same tags, and output what tags they have
$searchTags = explode(',', $currentTags);
$query = $tagsHelper->getTagItemsQuery($searchTags);
$db = Factory::getDbo();
$db->setQuery($query);
$results = $db->loadAssocList();
echo "<h4>Records with matching tags</h4>";
$tagLayout = new JLayoutFile('joomla.content.tags');
foreach ($results as $result)
{
	echo "<br>Content Type: " . $result["type_alias"] . ", Id: " . $result["content_item_id"] . ", Title: " . $result["core_title"] . "<br>";
	$itemTags = $tagsHelper->getItemTags($result["type_alias"] , $result["content_item_id"]);
	echo $tagLayout->render($itemTags);
}

?>
<form action="<?php echo JRoute::_('index.php?option=com_sample_tags'); ?>"
    method="post" name="sampleForm" id="adminForm" enctype="multipart/form-data">

	<?php echo $form->renderFieldset('tagset'); ?>
	
	<button type="submit">Submit</button>
</form>

tags_form.xml File containing the XML for the form definition.

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fieldset name="tagset">
		<field
			name="id"
			type="text"
			label="record id"
			size="4"
			class="inputbox"
			readonly="true"
			required="true" />
		<field
			name="title"
			type="text"
			label="title"
			size="40"
			class="inputbox"
			required="true" />
		<field 
			name="tags" 
			type="tag" 
			label="JTAG" 
			description="JTAG_DESC" 
			mode="ajax" 
			class="inputbox span12 small" 
			multiple="true" 
			/>
	</fieldset>
</form>

Related Material

J3.x:Using Tags in an Extension

J3.x:Developing an MVC Component/Adding Tags

Unified Content Model