How to add/update tagger tags to/from a resource programmatically?

I’m creating resources or updating existing ones by importing a csv with their data through FormIt and a custom hook. I use tagger to create categories for those resources. Now my question is: How can I assign/update tagger categories from my csv data to a resource?

I have a row, which contains the categories to be assigned:

$row['category'] = 'Cat1, Cat2, Cat3'

Assume the resource object (either newly created or an already existing one selected) is available as $resource.

If possible, it would be nice to check if a category exists already in tagger or if it has to be created. If that’s too much, all categories that might occur in $row can also be created beforehand through the manager. So that’s not my main concern.

There are 2 classes/database tables that are involved:

  • class TaggerTag (table modx_tagger_tags) → all the tags
  • class TaggerTagResource (table modx_tagger_tag_resources) → the assignment of tags to resources.

For your import to work you have to do the following:

  • Delete all existing assigned tags for this resource (TaggerTagResource where resource is equal to the current resource ID)
  • For every category in your list (‘Cat1, Cat2, Cat3’)
    1. Query the ID of this tag from TaggerTag
    2. Add a TaggerTagResource for the current resource with the correct tag ID.

So i’ve been trying to wrap my head around this all day and this is what I came up if. Honestly I was too afraid to actually test it altogether yet (just tested parts of it), as I didn’t want to delete stuff that shouldn’t be deleted.

Also still learning a lot about xpdo and the first thing I’m confused with is:
When do I use $modx-> and when $xpdo->? Or does this not make a difference? I had the occurance while testing in a snippet, that I got an error saying “xpdo is not available” of some sort.

Here is what I have so far, I would highly appreciate any advice on making it work in general and also later improving performance as this is part of an import script which will be executed probably multiple thousand times.

$categories = array('test1','test2','test3');  // can be this or array('');
$resourceId = 25;

// check if any categories are given
if (!in_array('', $categories, true)) {
  // clear existing tags
  $resourceTags = $xpdo->getCollection('TaggerTagResource', ['resource' => $resourceId]);
  foreach ($resourceTags as $resourceTag) {
    $resourceTag->remove();
  }               

  // assign tags
  foreach ($categories as $cat) {
    $c = $modx->query("SELECT id FROM modx_tagger_tags WHERE alias='$cat'");
    if (is_object($c)) {
      $catId = $c->fetch(PDO::FETCH_ASSOC);

      $tagResource = $xpdo->newObject('TaggerTagResource',array(
        'tag' => $catId,
        'resource' => $resourceID,
      ));
    }
  }
} else {
  // just clear tags
  // is this also a valid way to do this?
  $resourceTags = $xpdo->removeCollection('TaggerTagResource', ['resource' => $resourceId]);
}

In MODX just use $modx.

(You could theoretically use xPDO independently from MODX and create your own object of the xPDO class. I think that is why $xpdo is sometimes used in the documentation.)


  • Make sure your variable names are always exactly the same. First you use $resourceId = 25; with a lower case d, then later with an upper case D ('resource' => $resourceID,). That doesn’t work.

  • $catId = $c->fetch(PDO::FETCH_ASSOC); $catId is not the ID here. It’s an associative array with a key “id”. $catId["id"] gives you the ID.

  • You can’t create the “TaggerTagResource” like this.

$tagResource = $xpdo->newObject('TaggerTagResource',array(
	'tag' => $catId,
	'resource' => $resourceID,
));

(It’s a weird case, but the problem seems to be that both fields ‘tag’ and ‘resource’ are part of the primary key.)
So use syntax like this instead:

$tagResource = $modx->newObject('TaggerTagResource');
$tagResource->set('tag', $catId);
$tagResource->set('resource', $resourceId);
  • You also have to save the “TaggerTagResource” object after creating it:
if(!$tagResource->save()){
    $modx->log(modX::LOG_LEVEL_ERROR, "Couldn't create tag " . $catId . " for resource " . $resourceId);
}

I also had to load the Tagger package first to make it work.
Otherwise the “TaggerTagResource” isn’t known.

Something like:

$corePath = $modx->getOption('tagger.core_path', null, $modx->getOption('core_path', null, MODX_CORE_PATH) . 'components/tagger/');
$tagger = $modx->getService('tagger', 'Tagger', $corePath . 'model/tagger/', array('core_path' => $corePath));

Also, if by any chance you are using MODX 3, then the code has to be changed slightly, to account for the namespace.

1 Like

Thank you, this helped a lot! I think I have one issue left, at the part where I try to figure out, if the given category actually exists:

foreach ($categories as $cat) {
  $c = $modx->query("SELECT id FROM mx_tagger_tags WHERE alias='$cat'");
  if (is_object($c)) {
    $catArr = $c->fetch(PDO::FETCH_ASSOC);
    $catId = $catArr['id'];  
    //...

I thought I could check if the query $c returns anything by checking is_object($c) but if I understand correctly, then $c always returns an object, just an empty one if the query is empty?

When I check for the size of the result like mentioned here, my script breaks but I get no errors in the log:

if (!is_object($c)) {
  return false;
}

if (count($c) > 0) {
  $catArr = $c->fetch(PDO::FETCH_ASSOC);
  $catId = $catArr['id'];
  //...

What is wrong with this approach?

is there a reason, why you don’t use

$object = $modx->getObject('taggerTag',['alias' => $cat]);

if ($object){
.......
}
1 Like

Probably only my lack of xpdo knowledge… Thanks, this works great now! (Note: the class name is TaggerTag with a capital T)

If there’s anything else I could improve, especially in regards to performance, I would appreciate any hints. Here’s the full script:

$categories = array('test1','test2','test3');  // can be this or array('');
$resourceId = 25;

// clear existing categories
$resourceTags = $modx->removeCollection('TaggerTagResource', ['resource' => $resourceId]);

// check if any categories given
if (!in_array('', $categories, true)) {
  // assign categories
  foreach ($categories as $cat) {
    $tag= $modx->getObject('TaggerTag',['alias' => $cat]);

    if ($tag) {
      $catId = $tag->get('id');

      $tagResource = $modx->newObject('TaggerTagResource');
      $tagResource->set('tag', $catId);
      $tagResource->set('resource', $resourceId);

      if(!$tagResource->save()){
        $modx->log(modX::LOG_LEVEL_ERROR, 'Could not set tag ' . $catId . ' for resource ' . $resourceId);
      }
    } else {
      $modx->log(modX::LOG_LEVEL_ERROR, 'Category ' . $cat . ' does not exist!');
    }
  }
}

I’m not so sure about this check. If you have an array with a lot of elements and one of them is empty ($categories = array('','test2','test3', ..., 'test100');), the code won’t add any tags.


Removing all the tags and then adding them again is not efficient (but easier to implement).
If the same resources with the same tags get updated again and again, a different approach is better.

  1. Query all the existing tags of the resource and put them in an array.
  2. For each tag in the update, check if it already exists in the array.
    If it exists, delete it from the array.
    If it doesn’t exist, add it to the database.
  3. Delete the tags from the database that remain in the array.

That is what happens when you save a resource in the manager on the OnDocFormSave event.


If you don’t have that many tags and you update a lot of resources in a row, it will be faster if you load all the tags first and save them to an array, than making a new database query for every tag ($tag= $modx->getObject('TaggerTag',['alias' => $cat]);).

In general for a better performance, you want to avoid (unnecessary) database queries.