Invision Community 5 fires 58 event-listener hooks. None of them concern tags.
Tags do not go through a model with a save() you can observe. They arrive through Content\Taggable::setTags(), which deletes the item's existing rows in core_tags and inserts the new ones directly. No event, no queue job, no notification. So there is no supported way to be told that a tag has come into existence.
The trap this sets
Any application whose state is derived from the tag list has to refresh that state somehow, and the easy place to do it is wherever the administrator already is:
if ( $values = $form->values() )
{
$form->saveAsSettings( $values );
Palette::refresh(); // rebuild derived state
...
}
That is correct as far as it goes, and it is also the entire failure. If the only callers of your refresh are ACP controllers, then your app is up to date exactly when an administrator is looking at it and at no other time. A tag created by a member, arriving in an import, or added by another application is never picked up.
Worse, this fails invisibly and in the most misleading way possible: the ACP screen is always right, because visiting it is what makes it right. The administrator sees correct state on the settings page and wrong state on the front end, and concludes the app is broken rather than stale.
A tag-colouring application shipped exactly this. Colours were only ever assigned, and its stylesheet only ever rebuilt, from the admin screen. Every tag created after setup kept the theme's default colour indefinitely.
Do not solve it in the request cycle
core/Loader::onFinish() runs on every front-end request and is tempting. Resist it for this: detecting a new tag means a grouped query over core_tags, and running that on every page view of the community is not a reasonable price for a cosmetic feature. (For work that genuinely must happen per request, see Running code on every request, after the page has been sent.)
Solve it with a task
A scheduled sweep is what core itself uses for comparable housekeeping.
// data/tasks.json — the key must match the class and the filename
{"myappscan": "P0Y0M0DT0H15M0S"}
namespace IPS\myapp\tasks;
class myappscan extends \IPS\Task
{
public function execute(): ?string
{
try
{
$done = Thing::refreshDerivedState();
return $done ? "Updated {$done} things" : NULL;
}
catch ( \Throwable $e )
{
\IPS\Log::log( $e, 'myapp_scan' );
/* Swallowed deliberately: core disables a task that keeps throwing,
and losing the sweep permanently is worse than missing one run. */
return NULL;
}
}
public function cleanup(): void {}
}
Make the refresh free when nothing changed
🚨 A refresh written for a button press is usually written to assume something changed. Called every fifteen minutes, that becomes a settings write, and therefore a settings-cache invalidation across every node, four times an hour forever. Compare before writing:
public static function refresh(): string
{
$css = static::build();
$hash = $css === '' ? '' : substr( md5( $css ), 0, 10 );
if ( (string) Settings::i()->myapp_hash === $hash and (string) Settings::i()->myapp_css === $css )
{
return $hash; // nothing to do, and now genuinely free
}
Settings::i()->changeValues( array( 'myapp_css' => $css, 'myapp_hash' => $hash ) );
return $hash;
}
🚨 And the one that is easier to get wrong than any of the above
There are two tag tables and they mean different things:
core_tags_data | the tags defined for the community — what the ACP tag manager edits, and what Tag::getStore() returns (enabled only, recommended first, cached) |
core_tags | which items carry which tag — usage, not existence |
Listing "the community's tags" by grouping core_tags looks right and is a real, plausible list. It is also wrong: a tag created in the tag manager and not yet applied to any content has no rows there, so it is invisible. The same application above shipped a screen for choosing tag colours that could not show a newly created tag at all — which is backwards, because creating a tag is exactly when you want to pick its colour, before anybody sees it in the default one.
Depending on what you are building you usually want the union, not either one:
/* defined — authoritative for spelling, since core_tags stores whatever
case the item happened to be tagged with */
foreach ( \IPS\Content\Tag::getStore() as $tag )
{
$key = mb_strtolower( (string) $tag );
$text[ $key ] = (string) $tag;
$uses[ $key ] = $uses[ $key ] ?? 0;
}
Keep the usage side too: if the community allows free-form tags, members create tags that exist in core_tags and nowhere else.
The general shape is worth internalising. Any query of the form "distinct values in the usage table" silently means "things that have already happened", which is not the same as "things that exist". It fails quietly, because what it returns is genuinely a list of real tags — just not the list you asked for.
Two things that bite while wiring this up
A task file that is not declared in data/tasks.json is never registered and never runs, with nothing anywhere saying so — the same silent-manifest family as a malformed listeners.json. Worth a static check in both directions: every declared task has a file, and every file is declared.
🚨 A settings field takes its language string from its own key. If a key of that exact name already exists for something else, adding the setting silently relabels the other thing. A setting named myapp_auto where myapp_auto is already a button label renames the button, with no error and no warning — you find it by noticing the button says something odd. Grep dev/lang.php for a key before naming a setting after it.
Tell the administrator what is going to happen
Once the sweep is periodic, state is briefly stale by design. Say so where they will look: "3 tags have no colour yet — these will be coloured automatically within about fifteen minutes, or press Colour them for me now." Showing pending state without saying what will resolve it reads exactly like the bug you just fixed.
Verified against
Invision Community 5.0.19, by reading system/Content/Taggable.php, system/Task/Task.php, and the complete set of hooks in system/Events/Event.php.
Related application: Tag Colors — Tag Colours uses a scheduled scan for this reason, so a tag created by a member, by an import or by another application is picked up without an administrator opening the screen.
Recommended Comments