3

I am trying to tag some items (com_content.article) programmatically, and I have an issue while trying to use the TagsHelper class' tagItem method. I am not an expert in Joomla and I am a bit lost.

I get the following error (below the call stack):

1048 Column 'type_alias' cannot be null

It seems that the helper does not manage to get the type_alias (which should be com_content.article) from my $table object... Does anyone see how I could help it?

Here is my code:

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

[...]

$helpertags = new TagsHelper();
$table = Table::getInstance('Content', 'JTable', array());
$table->load($id_article);
$tags = array( 321, 43 ); // IDs of some tags I created or checked before
$helpertags->tagItem('com_content.article', $table, $tags, false);
$table->store();
1   ()  JROOT/libraries/vendor/joomla/database/src/Mysqli/MysqliStatement.php:435

2   Joomla\Database\Mysqli\MysqliStatement->execute()   JROOT/libraries/vendor/joomla/database/src/DatabaseDriver.php:675

3   Joomla\Database\DatabaseDriver->execute()   JROOT/libraries/src/Helper/TagsHelper.php:126

4   Joomla\CMS\Helper\TagsHelper->addTagMapping()   JROOT/libraries/src/Helper/TagsHelper.php:1043

5   Joomla\CMS\Helper\TagsHelper->tagItem()     JROOT/administrator/components/com_jsonems/tmpl/hello/default.php:151
2
  • Version Joomla 3 or 4?
    – Zollie
    Commented Mar 14, 2023 at 10:55
  • 1
    Version Joomla 4.2.6 Commented Mar 14, 2023 at 14:03

3 Answers 3

2

The shortest possible working version of this code is this as I tested this:

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

function addTagsToArticle($ucmId, $articleId, $tags) {

    $tagsHelper = new TagsHelper();

    $app = Factory::getApplication();
    $factory = $app->bootComponent('com_content')->getMVCFactory();

    $table = $factory->createTable('Article', 'Table');
    
    $table->load($articleId);

    $tagsHelper->typeAlias = $table->typeAlias;

    $tagsHelper->tagItem($ucmId, $table, $tags, false);

}

And you call this function like:

$newTags = ['#new#Millions', '#new#Worldwide'];

$ucmId = 5; // core_content_id of the article from ucm_content table

$itemID = 11; // article ID in #__content table

addTagsToArticle($ucmId, $itemID, $newTags);

This way the code works and save new tags to an article. without the store() method at the end, it is not needed, and the tags has to be with #new# prefix in the array.

THE LONG WAY OF DOING THE SAME

I have tested some codes more deeply regarding tagging content items in Joomla 4. I am not sure that too many people want to tag articles/content programmatically and I do not think that there is any official way of doing that but this is one way of doing that. Someone really worked out a quite good and complex tagging system for Joomla and I just took some parts from that core code and you can use it in your own custom methods.

First I created a function createNewTags() using the Joomla4 core createTagsFromField() from the TagsHelper class. I used that because it is not worthy to create similar function and it is sure that it works well on saving new tags. If a tag was already in the tag table, then it does not save it again. However, we have to modify the core addTagMapping() method for our custom code.

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\UCM\UCMType;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

/**
* create new tags by array of strings (texts)
* @param   array    $tags   Array of tags to be applied. 
*
* @return  array  -- created tags
*/

function createNewTags($tags = []) {

    $tagsHelper = new TagsHelper();

    $new_tags = array_map(fn($tag) => "#new#$tag", $tags);

    // create tags first and save it
    $tags_ids = $tagsHelper->createTagsFromField($new_tags);

    return $tags_ids;
}

// we have to create our own custom function for mapping
function addTagMapping($ucmId, $contentId, $table, $tags = [])
{
    $db = $table->getDbo();
    $item = $contentId;
    $ucm = new UCMType($table->typeAlias, $db);
    $typeId = $ucm->getTypeId();

    // Prevent saving duplicate tags
    $tags = array_values(array_unique($tags));

    if (!$tags) {
        return true;
    }

    $query = $db->getQuery(true);
    $query->insert('#__contentitem_tag_map');
    $query->columns(
        [
            $db->quoteName('core_content_id'),
            $db->quoteName('content_item_id'),
            $db->quoteName('tag_id'),
            $db->quoteName('type_id'),
            $db->quoteName('type_alias'),
            $db->quoteName('tag_date'),
        ]
    );

    foreach ($tags as $tag) {
        $query->values(
            implode(
                ',',
                array_merge(
                    $query->bindArray([(int) $ucmId, (int) $item, (int) $tag, (int) $typeId]),
                    $query->bindArray([$table->typeAlias], ParameterType::STRING),
                    [$query->currentTimestamp()]
                )
            )
        );
    }

    $db->setQuery($query);

    return (bool) $db->execute();
}

function tagThoseArticleItems($ucmId, $itemID, $ourTags = [], $replace = true) {

    $tagsHelper = new TagsHelper();  
    $tags = $tagsHelper->createNewTags($ourTags);

    $app = Factory::getApplication();
    $factory = $app->bootComponent('com_content')->getMVCFactory();

    $table = $factory->createTable('Article', 'Table');    
    
    $tagsHelper->typeAlias = $table->typeAlias;
    // $table->setTagsHelper($tagsHelper); // only if the table does not have it

    foreach ($tags as $tagId) {
        $tagsHelper->tagDeleteInstances($tagId);
    }
    
    // we have to use our own custom function on this
    // with added itemID (content item ID) to the call
    $result = addTagMapping($ucmId, $itemID, $table, $tags, $replace = true);

    return $result;
    
}

And you call this whole thing like:

$ourTags = ['getty', 'Roma', 'New York'];

// this is an existing article ID from the #__ucm_content table
$ucmId = 5;

$itemID = 11; // The article ID in #__content table

$our_tags_created = tagThoseArticleItems($ucmId, $itemID, $ourTags, $replace = true);

The second function is just doing the tag mapping correctly. I hardcoded the content Table in this, but you can refactor this for using other tables too, if not content tagging.

Important to note that $ucmId is the core_content_id from the #__ucm_content table. so you should use that ID as the first argument and NOT a type like com_content.article and not the article ID from the #__content table. In our custom code we have to provide the article ID too from the content table.

The challenge

I tried but I have not found a short, straightforward solution on this task in Joomla on tagging content articles from an outside extension for example. Anybody else on this? The question is interesting because if someone came with a Wordpress developer background, rightfully would ask and look for a one line solution on this, like WP has on tagging a Post:

wp_set_post_tags( int $post_id, string|array $tags = '', bool $append = false )

Unfortunately at many of these small points we could realize the big difference on developer friendliness of the Content Management Systems.

2
  • 1
    Thank you very much for your complete answer. I made several tests and ended up with similar issues. My final option was to directly register the tag association in the DB. Thanks again Commented Mar 29, 2023 at 7:36
  • There's an issue with these testing under J5.1. The entry in #__ucm_content is only created when an article is saved with a tag so it has no core_content_id. An article that has not yet been tagged simply doesn't have an entry in the table yet, so you can't get a $ucmid to pass to your function.
    – RogerCO
    Commented Jun 8 at 10:15
0

The error message "Column 'type_alias' cannot be null" suggests that the type_alias field in the #__content_types table is not being set correctly when you are trying to tag the content item.

To fix this issue, you can try setting the type_alias field of your $table object explicitly before calling the tagItem() method. You can set the type_alias field to 'com_content.article' which is the expected value for this type of content item.

Here is how you can modify your code to set the type_alias field:

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

[...]

$helpertags = new TagsHelper();
$table = Table::getInstance('Content', 'JTable', array());
$table->load($id_article);
$table->type_alias = 'com_content.article'; // set the type_alias field
$tags = array( 321, 43 ); // IDs of some tags I created or checked before
$helpertags->tagItem('com_content.article',$table,$tags, false);
$table->store();
1
  • Thank you for your answer. I have tried this before, but it does not make a difference... Commented Mar 13, 2023 at 14:28
0

Here's a simple solution that uses the batch() command in AdminModel and works ok even if the item doesn't yet have ucm core_content_id

  /**
   * @name addTagToItem()
   * @desc adds an existing tag to an item
   * @param string $compitem - the component item type
     in dotted lower case eg com_content.article
   * @param int $itemId - the id of the ite the tag is being added to
   * @param int $tagId - the id of the tag being added
   * @return boolean - true on success, false on failure
   */
  public function addTagToItem($compitem, $itemId, $tagId) {
    $arr=explode('.',$compitem);
    $app = Factory::getApplication();
    $factory = $app->bootComponent($arr[0])->getMVCFactory();
    $model = $factory->createModel(ucfirst($arr[1]), 'Administrator');
    $commands = array('tag'=>$tagId);
    $pks = [$itemId];
    $contexts = [$itemId=>$compitem.'.'.$itemId];
    $res = $model->batch($commands, $pks, $contexts );
    return $res;
}

Seems to work ok for me. I think tags are about the only thing left using the ucm system. Hopefully it'll get removed soon.

You have to use your own custom getCreateTag() function to get the tagId if you don't have it or if it is a new tag. Easier than farting around with #new# prepended to the tag title. Or use the very simple function in @Zollie answer above.

You could adapt to handle multiple tags and/or items by passing them in as an array and iterating through them building the $contexts array.

Same code could be used to remove tags if your component has implemented an batch.untag command (which IMHO should be part of core Joomla anyway)

1
  • Thanks for the tip. To solve the problem I used direct database functions but if I have to work it around again I will try your solution Commented Jun 10 at 8:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.