The Tags facade
Everything goes through one facade, RoundlyConsulting\Tags\Facades\Tags (global alias Tags), sugar over the TagsManager singleton. Types are declared on it, type() reaches the catalog of one type, for() one model’s tags, and the flat verbs tag a model and return what really changed:
use RoundlyConsulting\Tags\Facades\Tags;
// Types — declared once, at boot
Tags::define('genre')->hierarchical()->single();
Tags::types(); // every type, resolved
// The catalog of one type, optionally within a scope
$genres = Tags::type('genre');
$jazz = $genres->make('Jazz')->under($music)->create();
$genres->tree();
Tags::type('label')->within($team)->suggest('liv');
// One model's tags — a chainable handle
Tags::for($concert)->sync($jazz, 'genre')->attach(['Live', 'Outdoor'], 'label');
Tags::for($concert)->of('label');
Tags::for($concert)->findMany($validatedIds, 'label'); // keys a form validated, in the model's scopes
// Flat verbs return what really changed
$changes = Tags::attach($concert, ['live'], 'label'); // TagChanges
Tags::detach($concert, ['outdoor'], 'label');
Tags::sync($concert, null, 'genre'); // clears the category
Tags::clear($concert); // every tag of every type
// Any tag by key, ancestors for a whole page, maintenance
Tags::find($key);
Tags::loadAncestors($concerts->map(fn (Concert $concert) => $concert->tagOf('genre')));
Tags::repair();Manager methods
| Method | Returns | Does |
|---|---|---|
define($type) | TagTypeBuilder | Declare a type, from a service provider’s boot(). A string or a string-backed enum; a second definition throws. |
extend($type, Closure $callback) | void | Adjust a type from app code, whatever the provider order. |
types() | Collection<string, TagType> | Every defined type, resolved, keyed by name. |
type($type) | TypeTags | The catalog of one type — create, find, move, suggest… Unknown names throw. |
find($key) / findOrFail($key) | ?Tag / Tag | A live tag of any type by primary key. |
for($taggable) | TaggableTags | One model’s tags — a chainable handle. |
attach($taggable, $tags, $type = null) | TagChanges | Add tags; unknown free-form names are created in the model’s scope. |
detach($taggable, $tags, $type = null) | TagChanges | Remove tags; names that name no tag are ignored. |
sync($taggable, $tags, $type = null) | TagChanges | Make the model’s tags of one type (the default type when none is given) exactly these. |
clear($taggable, $type = null) | TagChanges | Remove every tag of a type, or of every type. |
loadAncestors(iterable $tags) | void | Load the ancestors of many tags in one query; nulls are skipped. |
repair(?RepairOptions $options = null) | RepairReport | Repair paths, scope keys, orphan taggings and usage counts. |
Tags::fake() | TagsFake | Facade only — the recording, still-performing fake (see Testing). |
perform() is public on the manager too, but internal: it is the single funnel every flat verb, handle, builder and model method runs its action through, and the hook the fake records with. Don’t call it from app code.
Tags::type($type) — the catalog
type() returns an immutable TypeTags handle for one type, optionally narrowed with within($scope). Reads see the scope’s tags plus the global ones; writes match the scope exactly — a scoped handle never writes a global tag, an unscoped one writes global tags only:
| Method | Returns | Does |
|---|---|---|
within(?Model $scope, bool $includeGlobal = true) | TypeTags | The same catalog within a scope: reads see the scope’s tags plus the global ones (or only the scope’s), writes go to the scope. |
definition() | TagType | The resolved type. |
make($name) | TagBuilder | Start a new tag; a string is the current locale, an array a locale map. |
create(TagData $data) | Tag | The builder’s code path, from a DTO. A deleted tag holding the name comes back with these attributes. |
findOrCreate(string|array|TagData $name) | Tag | The tag the name names (in the scope, else globally) unchanged, or a new one in the scope; a deleted tag holding the name comes back as it was. Allowed on curated types. |
edit(Tag $tag) | TagEditor | Change a tag fluently, then save(). |
update(Tag $tag, UpdateTagData $data) | Tag | Rename, or change the description, manual slug, colour or icon. |
rename(Tag $tag, string|array $name) | Tag | A string renames the current locale; a map merges per locale. |
move(Tag $tag, ?Tag $under = null, ?int $position = null) | Tag | A new parent (null: a root), optionally a position among the new siblings. |
reorder(?Tag $parent, array $order) | Collection<int, Tag> | Order the siblings under a parent (the roots when null): listed first, then the rest. |
delete(Tag $tag, TagChildren $children = Refuse) | Tag | Soft-delete and detach everywhere; children refuse, reparent or delete. |
restore(Tag $tag) | Tag | Bring a deleted tag back, without its old taggings, where its parent is now; refused under a deleted parent or past the depth cap. |
find($tag) / findOrFail($tag) | ?Tag / Tag | A live tag in the readable scopes: a model, a key (int), or a name or slug (string), the scope’s before a global one. |
findTrashed($tag) | ?Tag | A deleted tag of exactly this handle’s scope — the scope a restore writes to. |
findMany(array $keys) | Collection<int, Tag> | Live tags by key, in the order given; malformed, missing or foreign keys are skipped. |
all() / roots() | Collection<int, Tag> | Every live tag, or the roots, by position. |
tree(?Tag $root = null) | list<TagNode> | The tree, or one subtree, in one query. |
suggest(string $term, ?int $limit = null) | Collection<int, Tag> | Autocomplete across locales. |
popular(?int $limit = null, ?string $on = null, ?Closure $where = null) | Collection<int, Tag> | The most used tags — by usage count, or live on one class. |
query() | Builder<Tag> | Live tags of the type in the readable scopes. |
The TagBuilder
make($name) returns a TagBuilder. It validates eagerly where it can — a colour, an icon, a parent of another type or scope throws at the call — and both terminals build a TagData, so the builder and the DTO run the same code:
| Method | Returns | Does |
|---|---|---|
description($description) | TagBuilder | Plain text; a string is the current locale, an array a locale map. |
slug($slug) | TagBuilder | A manual slug, already in slug form — curated types only. |
color(?string $color) / icon(?string $icon) | TagBuilder | A #rrggbb colour (stored lower-case); an identifier or an emoji. |
under(?Tag $parent) / position(?int $position) | TagBuilder | A live parent of the same hierarchical type and scope — under(null) asks for a root; not called, a new tag is a root and a deleted tag the name brings back stays where it was. A position 0–2147483647 among its siblings. |
create() / findOrCreate() | Tag | Create — a deleted tag holding the name comes back with these attributes — or find the named tag unchanged and create only when missing; a deleted tag findOrCreate() brings back keeps its own attributes. |
The TagEditor
edit($tag) returns a TagEditor — only what you call changes:
| Method | Returns | Does |
|---|---|---|
name($name) | TagEditor | A string renames the current locale; a map merges per locale. |
description(?$description) | TagEditor | Merges per locale; a blank or null entry (['sk' => null]) clears that locale, null clears every locale. |
slug($slug) | TagEditor | A manual slug — curated types only. |
color(?string) / icon(?string) | TagEditor | null clears. |
save() | Tag | Builds an UpdateTagData and calls update(); only what was called changes. |
Tags::for($model) — one model’s tags
for() returns a TaggableTags handle. Writes go through the flat verbs — so the fake records them, and the model’s loaded tags relation is dropped so a chained read sees the change — and return the handle for chaining. findMany() turns keys a form validated into tags, in the model’s own scopes:
| Method | Returns | Does |
|---|---|---|
attach($tags, $type = null) | TaggableTags | Through Tags::attach(), which drops the model’s loaded tags relation after the write. |
detach($tags, $type = null) | TaggableTags | Through Tags::detach(). |
sync($tags, $type = null) | TaggableTags | Through Tags::sync(). |
clear($type = null) | TaggableTags | Through Tags::clear(). |
all() | Collection<int, Tag> | Every tag of the model, by position — the loaded relation when loaded. |
of($type) / one($type) | Collection / ?Tag | The tags of one type, or its one tag (a category). |
has($tag, $type = null) | bool | Whether the model carries a tag: a model, a key, a name or a slug along the locale chain. |
hasAny($tags, $type = null) / hasAll(…) | bool | Any or every one of them. |
findMany(array $keys, $type) | Collection<int, Tag> | Live tags of a type by key, in the scopes this model reads (its tagScope() and the global tags; global only for a global-only type), in the order given — the recipe for keys TagsRule::for($type)->byId()->taggable($model) validated. |
model() | Model&Taggable | The model behind the handle. |
The HasTags methods (attachTags(), tagsOf(), …) are shorthands over this handle, so everything here — the fake included — applies to them too.
Data objects
Every DTO is final readonly:
| DTO | Fields | Meaning |
|---|---|---|
TagData | name, description = [], slug = [], color = null, icon = null, parent = NotGiven, position = null | A new tag. Names, descriptions and manual slugs are locale maps; a string is the current locale. parent left out makes a new tag a root and leaves a deleted tag the name brings back where it was; null asks for a root. |
UpdateTagData | name, description, slug, color, icon | Every field defaults to NotGiven and stays; null clears a description, colour or icon; a name or description map merges per locale, and a blank or null description entry clears that locale only. |
TagChanges | taggable, attached, detached, created, restored | What a tagging write really changed; isEmpty(), ofType($type). |
TagSelection | tags, type | A tagging call’s input; TagSelection::of($tags, $type) for actions. |
TagNode | tag, children | One node of tree(), children in position order. |
RepairOptions | paths = true, scopeKeys = true, counts = true, orphans = true, type = null, dryRun = false | What Tags::repair() fixes. |
RepairReport | paths, cycles, scopeKeys, counts, orphans, unresolvableTypes, dryRun, trashedTaggings | What a repair found and fixed; isClean(). |
The PHP API trusts its caller
The facade enforces the domain rules — types, limits, curated lists, scopes, the tree — but never asks a Gate. The abilities in tags.authorization guard the JSON API only; code calling the facade decides who may call what.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.