Names, slugs and translations
A string names the tag whose slug, in some locale of the chain, equals that locale’s slug of the string — computed with sluggable-for-laravel’s own pipeline. So spelling, case and punctuation never make a second tag:
$rock = Tags::type('label')->findOrCreate('Rock');
Tags::type('label')->findOrCreate('ROCK!')->is($rock); // true — one tag per slug
Tags::type('label')->findOrCreate(' rock. ')->is($rock); // true
Tags::type('label')->find('rock')->is($rock); // true — a slug names its tag
// The dictionary keeps these three tags apart: c, c-plus-plus, c-sharp
Tags::type('label')->findOrCreate('C');
Tags::type('label')->findOrCreate('C++');
Tags::type('label')->findOrCreate('C#');- A name is trimmed, control and invisible characters are removed and whitespace collapses to one space. A blank name, or one longer than tags.names.max_length, throws InvalidTagException; so does a name with nothing to build a slug from.
- The locale chain is the current locale, then the fallback locale, then every other supported one. Each locale slugs with its own transliteration — Über is ueber in German, uber in English.
- The same slug in two locales may be two tags (gift is a present in English and poison in German): the earlier locale of the chain wins.
- Slugs are unique per type, scope and locale, deleted tags included, and never suffixed: a taken slug is the same tag, or a duplicate — never rock-2.
Every locale
A plain string is the current locale; a locale map sets several at once. Malformed locale keys throw, and keys outside translatable.locales are dropped — a package cannot know the app’s locales. The current locale must be a supported one, or a plain string cannot name a tag:
// A plain string is the current locale; a map sets several at once
$music = Tags::type('genre')->make(['en' => 'Music', 'sk' => 'Hudba'])->create();
$music->name; // 'Music' in en, 'Hudba' in sk
$music->slugFor('sk'); // 'hudba'
$music->getTranslations('name'); // ['en' => 'Music', 'sk' => 'Hudba']
app()->setLocale('sk');
Tags::type('genre')->find('music')->is($music); // true — the chain falls back to en
Tags::type('genre')->find('Hudba')->is($music); // true
// A rename changes only the locales it names; each slug follows its own name
Tags::type('genre')->rename($music, ['sk' => 'Hudobné žánre']);Duplicates and reserved names
use RoundlyConsulting\Tags\DataTransferObjects\TagData;
$pop = Tags::type('label')->make('Pop')->create();
Tags::type('label')->make('POP')->create();
// DuplicateTagException: A tag with the slug [pop] in [en] already exists.
$rock = Tags::type('label')->make('Rock')->color('#000000')->create();
Tags::type('label')->delete($rock);
// A deleted tag keeps its name: creating it again restores that tag, with the new attributes
$again = Tags::type('label')->create(new TagData(name: 'Rock', color: '#ffffff'));
$again->is($rock); // true — TagRestored fires, and it carries no old taggings
// Renaming another tag onto a deleted tag's slug is refused
Tags::type('label')->rename($pop, 'Rock');
// DuplicateTagException: A deleted tag holds the slug [rock] in [en]: restore that tag instead.A deleted tag keeps its name. Using the name again restores that tag instead of making a new one — always without its old taggings, so old records are never relabelled. What it comes back as depends on the call:
use RoundlyConsulting\Tags\DataTransferObjects\TagData;
$genres = Tags::type('genre');
$genres->delete($jazz); // Jazz sat under Music, with a colour, an icon and a description
// Any one of these brings the deleted Jazz back, without its old taggings:
$genres->findOrCreate('jazz'); // as it was — names, colour, icon, description, under Music
$genres->make('Jazz')->color('#ffffff')->findOrCreate(); // still as it was: of the builder's data only a parent applies
$genres->make('Jazz')->under($rock)->findOrCreate(); // as it was, moved under Rock
$genres->create(new TagData(name: 'Jazz', color: '#ffffff')); // with exactly these attributes (no icon, no description), under Music
$genres->make('Jazz')->under(null)->create(); // with these attributes, as a root
// With Music deleted too, create() and findOrCreate() refuse (InvalidTagHierarchyException,
// "restore it first") unless a parent is given; a free-form name comes back as a root instead
$article->attachTags('Physics', 'topic'); // 'topic' is hierarchical()->freeForm()- findOrCreate() — a name, a locale map, the builder or a TagData — and a free-form name in attachTags() or syncTags() bring it back as it was: every name, its description, slugs, colour and icon, under its parent’s current place. Of the builder’s data only a parent given with under() applies.
- create() applies its own attributes — names, description, manual slugs, colour and icon, so what it leaves out is cleared — and keeps the tag where it was unless a parent is given (under(null), or parent: null, asks for a root); a given position applies too.
- If its parent is deleted as well, create() and findOrCreate() refuse with InvalidTagHierarchyException (“restore it first”) unless a parent is given, while a free-form name brings it back as a root, like every free-form tag of a tree.
- Either way the type’s depth cap holds, its deleted descendants’ paths follow it, and it never goes under one of its own children (a plain $tag->delete() can leave live ones). A create whose locales are held by two different deleted tags is refused.
Stable and manual slugs
Without stableSlugs(), a locale’s slug follows its name on every rename — the old slug stops naming the tag. With stableSlugs() (curated types only), slugs never regenerate, so a category URL never moves — only a locale named for the first time gets a slug, once; the new name’s slug names the tag only once a manual slug says so. A rename onto a deleted tag’s slug points to restore() on a stable-slug type too:
use RoundlyConsulting\Tags\DataTransferObjects\UpdateTagData;
Tags::define('genre')->hierarchical()->stableSlugs();
$jazz = Tags::type('genre')->make('Jazz')->create();
$renamed = Tags::type('genre')->rename($jazz, 'Jazz music');
$renamed->name; // 'Jazz music'
$renamed->slug; // 'jazz' — /events?category=jazz still works
// Curated types also take a manual slug, already in slug form
$blues = Tags::type('genre')->make('Blues and soul')->slug('blues')->create();
Tags::type('genre')->update($blues, new UpdateTagData(slug: 'blues-soul'));A manual slug is for curated types only (InvalidTagException on a free-form type) and must already be in slug form — lowercase letters, digits and separators, at most tags.slugs.max_length. A slug another tag holds is a DuplicateTagException.
Descriptions, colours and icons
$music = Tags::type('genre')
->make(['en' => 'Music', 'sk' => 'Hudba'])
->description(['en' => 'Concerts and festivals.', 'sk' => 'Koncerty a festivaly.'])
->color('#F24E29') // a hex colour, stored lower-case: #f24e29
->icon('music') // an identifier (music, fa-guitar) or an emoji, at most 64 characters
->create();
Tags::type('genre')->edit($music)->color(null)->icon(null)->save(); // null clears- A description is plain text — no markup, no control characters other than line breaks and tabs — of at most tags.descriptions.max_length characters per locale. Blank locales are dropped.
- A colour is a #rrggbb hex value, stored lower-case.
- An icon is an identifier (music, fa-guitar) or an emoji: at most 64 characters, no whitespace, control characters, quotes or angle brackets.
- A tag’s type and owner never change: updating them throws InvalidTagException. Moving a tag between scopes is not supported.
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.