<p>Before worrying about how templating should work, let’s just get one thing out of the way: Computing this data every time it’s requested is horribly inefficient, and could easily take a site down. You’ll absolutely need to find a way to store the tag counts in a more permanent location, and calculate it when entries are saved rather than when the template requests it. (Even if you wrap the template in a <code>{% cache %}</code> tag, you’re still forcing a couple people to experience slow page load times for no good reason.)</p>

<p>You have multiple options on how to store the data, but giving it its own table is probably the most obvious/reliable. I’d call it something like <code>tagcounts</code>, and give it <code>tagId</code> and <code>count</code> columns. <code>tagId</code> should be a FK to the <code>tags</code> table, set to cascade delete.</p>

<p>Then you’d want to listen to the <a href="http://buildwithcraft.com/docs/plugins/events-reference#entries-onSaveEntry" rel="noreferrer">entries.onSaveEntry</a> event. When it fires, loop through all the entry’s Tags fields, and each of their selected tags. For each one, get the total count of rows in the <code>relations</code> table where the <code>targetId</code> column is set to the tag’s ID. Use DbCommand::insertOrUpdate() to save the value in your table.</p>

<pre><code>craft()-&gt;db-&gt;createCommand()-&gt;insertOrUpdate('tagcounts', array(
    'tagId' =&gt; $tag-&gt;id
), array(
    'count' =&gt; $count
));
</code></pre>

<p>You’ll also need to loop through all existing tags and populate this table when the plugin is first installed.</p>

<p>With the tag count data in place, next up is templating. Unfortunately I don’t have a good answer for you there. It’s a complicated problem.</p>

<p>I think what I would do is, create a <a href="http://buildwithcraft.com/docs/plugins/variables" rel="noreferrer">Variable</a> class which accepts an ElementCriteriaModel, passes that off to <code>craft()-&gt;elements-&gt;buildElementsQuery()</code> which will take most of the parameters into account, then make some additional modifications to the query – join in the <code>tagcounts</code> table and set the <code>ORDER BY</code>.</p>

<p>Next up you would basically have to copy a ton of code from ElementsService::findElements(), which is totally lame. I’m going to see what I can do to make it much easier on you going forward. Ideally at this point you would be able to just do something like:</p>

<pre><code>return craft()-&gt;elements-&gt;findElementsUsingQuery($query);
</code></pre>

CC BY-SA 3.0
2014-07-16T23:57:17.610Z
Oh sure... now that there's a *bounty* you answer.
Brad Bell
CC BY-SA 3.0
2014-07-17T00:08:54.587Z
Thanks for breaking this down, Brandon! The last two paragraphs are where I locked up, and it sounds like there *isn't* a nice clean way of getting findElements() functionality without just copying it. At least I know I didn't miss something obvious this time.
Matt Stein
CC BY-SA 3.0
2014-07-16T23:51:36.290Z
2014-07-16T23:51:36.290Z
Brandon Kelly
<p>I'm attempting to write a plugin to extend tags by adding a <code>count</code> property that'd be useful <a href="https://craftcms.stackexchange.com/questions/1086/order-tags-by-count-e-g-top-10-tags-used">identifying popular tags</a>, <a href="https://craftcms.stackexchange.com/questions/881/how-can-i-create-a-visually-weighted-tag-cloud">making a tag cloud</a>, and anything else where a count would be useful.</p>

<h2>The Goal</h2>

<p>What I'd like is to neatly extend <code>TagModel</code> so that interacting with these new count tallies  would be straightforward and as similar as possible to working with tags. For example, pretend <code>superTags</code> is my count-appended set and I'm getting the top ten tags for a site across sections:</p>

<pre><code>{% for tag in superTags.limit(10).order('count desc') %}
    {{ tag.name }} has been used {{ tag.count }} times.
{% endfor %}
</code></pre>

<p>...or getting the top 10 tags by a blog author:</p>

<pre><code>{% for tag in superTags.channel('blog').relatedTo(author).limit(10).order('count desc') %}
    {{ tag.name }} was born in {{ tag.dateCreated | date('Y') }}.
{% endfor %}
</code></pre>

<h2>The Problem</h2>

<p>I humbly admit that I'm struggling to see the big picture. For example, this works fine (my plugin's called Taggregate):</p>

<pre><code>{% set tags = craft.tags.find() %}
{% set superTags = craft.taggregate.appendCounts(tags) %}
{% set superTags = craft.taggregate.sortByCount(superTags) %}

{% for tag in superTags %}
    {{ tag.name }} has been used {{ tag.count }} times.&lt;br&gt;
{% endfor %}
</code></pre>

<p>I've got a <code>TaggregateModel</code> that extends <code>TagModel</code> only to merge in a <code>count</code> Number to <code>defineAttributes()</code>. My Service takes the tags array, then spits out a new one with <code>TaggregateModel</code> objects that include the <code>count</code> property. (Each count is just a tally from <code>craft_relations</code> with no regard for other parameters like section or author—later I intend to use an aggregate table as Brad suggested, even though that's irrelevant here.)</p>

<p><strong>TaggregateService</strong></p>

<pre><code>...

public function appendCounts($tags)
{
    $superTags = array(); 

    new TaggregateModel($tags);

    for ($i=0; $i &lt; count($tags); $i++)
    { 
        $superTags[$i] = new TaggregateModel($tags[$i]);
        $superTags[$i]-&gt;count = $this-&gt;getTagCount($superTags[$i]-&gt;id);
    }

    return $superTags;
}

public function getTagCount($tagId)
{
    $q = craft()-&gt;db-&gt;createCommand('SELECT count(*) as count FROM craft_relations WHERE targetId = '.$tagId)-&gt;queryAll();

    // TODO: get stored aggregate if it exists, otherwise count and store

    return $q[0]['count'];
}

public function sortByCount($tags)
{
    usort($tags, function($a, $b)
        {
            return $a-&gt;count &lt; $b-&gt;count;
        }
    );

    return $tags;
}

...
</code></pre>

<p>While this works, it's clearly bad design:</p>

<ol>
<li>The Twig interface is specific to the plugin's weird version of non-native methods.</li>
<li>This <code>superTag</code> array isn't smart enough to do any of the cool things that <code>TagModel</code> can.</li>
</ol>

<h2>The Question</h2>

<p><strong>Is there a more sensible way to build this so I can hit my ideal usage outlined in "The Goal" above?</strong> I'm not looking for somebody to write this for me, just some big-picture guidance since I suspect I'm making this harder than it should be. </p>

<p><strong>...or is my seemingly-simple objective actually complicated?</strong></p>

<plugin-development><tags>
How can I best extend TagModel to add a `count` property?
# How can I best extend TagModel to add a `count` property?

(Asked by: username_0 on 2014-07-14)

I'm attempting to write a plugin to extend tags by adding a count property that'd be useful identifying popular tags, making a tag cloud, and anything else where a count would be useful.
The Goal
What I'd like is to neatly extend TagModel so that interacting with these new count tallies  would be straightforward and as similar as possible to working with tags. For example, pretend superTags is my count-appended set and I'm getting the top ten tags for a site across sections:
{% for tag in superTags.limit(10).order('count desc') %}
    {{ tag.name }} has been used {{ tag.count }} times.
{% endfor %}

...or getting the top 10 tags by a blog author:
{% for tag in superTags.channel('blog').relatedTo(author).limit(10).order('count desc') %}
    {{ tag.name }} was born in {{ tag.dateCreated | date('Y') }}.
{% endfor %}

The Problem
I humbly admit that I'm struggling to see the big picture. For example, this works fine (my plugin's called Taggregate):
{% set tags = craft.tags.find() %}
{% set superTags = craft.taggregate.appendCounts(tags) %}
{% set superTags = craft.taggregate.sortByCount(superTags) %}

{% for tag in superTags %}
    {{ tag.name }} has been used {{ tag.count }} times.
{% endfor %}

I've got a TaggregateModel that extends TagModel only to merge in a count Number to defineAttributes(). My Service takes the tags array, then spits out a new one with TaggregateModel objects that include the count property. (Each count is just a tally from craft_relations with no regard for other parameters like section or author—later I intend to use an aggregate table as Brad suggested, even though that's irrelevant here.)
TaggregateService
...

public function appendCounts($tags)
{
    $superTags = array(); 

    new TaggregateModel($tags);

    for ($i=0; $i < count($tags); $i++)
    { 
        $superTags[$i] = new TaggregateModel($tags[$i]);
        $superTags[$i]->count = $this->getTagCount($superTags[$i]->id);
    }

    return $superTags;
}

public function getTagCount($tagId)
{
    $q = craft()->db->createCommand('SELECT count(*) as count FROM craft_relations WHERE targetId = '.$tagId)->queryAll();

    // TODO: get stored aggregate if it exists, otherwise count and store

    return $q[0]['count'];
}

public function sortByCount($tags)
{
    usort($tags, function($a, $b)
        {
            return $a->count < $b->count;
        }
    );

    return $tags;
}

...

While this works, it's clearly bad design:

The Twig interface is specific to the plugin's weird version of non-native methods.
This superTag array isn't smart enough to do any of the cool things that TagModel can.

The Question
Is there a more sensible way to build this so I can hit my ideal usage outlined in "The Goal" above? I'm not looking for somebody to write this for me, just some big-picture guidance since I suspect I'm making this harder than it should be. 
...or is my seemingly-simple objective actually complicated?

**Tags:** plugin-development, tags

---

## 1 answers

### Answer by username_1 on 2014-07-16. Score: 5 | Accepted answer

Before worrying about how templating should work, let’s just get one thing out of the way: Computing this data every time it’s requested is horribly inefficient, and could easily take a site down. You’ll absolutely need to find a way to store the tag counts in a more permanent location, and calculate it when entries are saved rather than when the template requests it. (Even if you wrap the template in a {% cache %} tag, you’re still forcing a couple people to experience slow page load times for no good reason.)
You have multiple options on how to store the data, but giving it its own table is probably the most obvious/reliable. I’d call it something like tagcounts, and give it tagId and count columns. tagId should be a FK to the tags table, set to cascade delete.
Then you’d want to listen to the entries.onSaveEntry event. When it fires, loop through all the entry’s Tags fields, and each of their selected tags. For each one, get the total count of rows in the relations table where the targetId column is set to the tag’s ID. Use DbCommand::insertOrUpdate() to save the value in your table.
craft()->db->createCommand()->insertOrUpdate('tagcounts', array(
    'tagId' => $tag->id
), array(
    'count' => $count
));

You’ll also need to loop through all existing tags and populate this table when the plugin is first installed.
With the tag count data in place, next up is templating. Unfortunately I don’t have a good answer for you there. It’s a complicated problem.
I think what I would do is, create a Variable class which accepts an ElementCriteriaModel, passes that off to craft()->elements->buildElementsQuery() which will take most of the parameters into account, then make some additional modifications to the query – join in the tagcounts table and set the ORDER BY.
Next up you would basically have to copy a ton of code from ElementsService::findElements(), which is totally lame. I’m going to see what I can do to make it much easier on you going forward. Ideally at this point you would be able to just do something like:
return craft()->elements->findElementsUsingQuery($query);

> **Comments:**
> **username_2**: Oh sure... now that there's a *bounty* you answer.  
> **username_0**: Thanks for breaking this down, Brandon! The last two paragraphs are where I locked up, and it sounds like there *isn't* a nice clean way of getting findElements() functionality without just copying it. At least I know I didn't miss something obvious this time.  

---

