spatie/laravel-tags
Add flexible tagging to Laravel Eloquent models with the HasTags trait. Create, attach, detach, and query tags with ease, with built-in support for tag types, translations, and sorting—ideal for organizing content across your app.
Installation
composer require spatie/laravel-tags
php artisan migrate
tags and taggables tables automatically.Apply Trait
Add HasTags to any Eloquent model:
use Spatie\Tags\HasTags;
class Article extends Model
{
use HasTags;
}
First Use Case Attach tags during model creation:
$article = Article::create([
'title' => 'Getting Started with Laravel Tags',
'tags' => ['laravel', 'tags', 'spatie']
]);
Tag Management
attachTag(), detachTag(), or bulk methods (attachTags(), detachTags()).
$article->attachTag('new-tag');
$article->detachTag('old-tag');
syncTags() or scoped types:
$article->syncTags(['updated', 'tags']);
$article->syncTagsWithType(['categories'], 'type');
Querying Models by Tags
// Models with ANY of these tags
Article::withAnyTags(['laravel', 'php'])->get();
// Models with ALL tags
Article::withAllTags(['laravel', 'tags'])->get();
// Models without specific tags
Article::withoutTags(['deprecated'])->get();
// Scoped by tag type
Article::withAnyTagsOfType('categories')->get();
Tag Types
categories, topics):
$article->attachTag('backend', 'categories');
$article->tagsWithType('categories'); // Retrieve typed tags
Translations
setTranslation():
$tag = Tag::findOrCreate('tag');
$tag->setTranslation('name', 'fr', 'étiquette');
$tag->save();
// Fetch translated tags
$article->tagsTranslated('fr');
Sorting
order_column:
$tag1 = Tag::findOrCreate('first');
$tag2 = Tag::findOrCreate('second');
$tag1->swapOrder($tag2); // Swap positions
Form Handling
syncTags() in form submissions to avoid duplicate tags:
$request->validate(['tags' => 'array']);
$article->syncTags($request->tags);
API Responses
return Article::with(['tagsTranslated' => function ($query) {
$query->withTranslation('name');
}])->find($id);
Admin Panels
hasTag() for conditional UI:
@if($article->hasTag('featured'))
<span class="badge">Featured</span>
@endif
Testing
$tag = Tag::findOrCreate('test-tag');
$article = Article::factory()->create()->attachTag($tag);
Tag Creation Race Conditions
findOrCreate() may create duplicate tags if called concurrently. Use transactions or firstOrCreate() with unique constraints:
Tag::firstOrCreate(['name' => 'tag'], ['slug' => Str::slug('tag')]);
Tag Type Scope Confusion
withAnyTagsOfType() requires exact type matches. Typos (e.g., 'category' vs 'categories') return empty results.Translation Overwrites
PostgreSQL JSON Limitations
jsonb operations.Dynamic Table Names
taggable_type column matches your model’s fully qualified class name.Query Logs
DB::enableQueryLog();
Article::withAnyTags(['laravel'])->get();
dd(DB::getQueryLog());
Tag Existence
if (!$article->hasTag('missing-tag')) {
$article->attachTag('missing-tag');
}
Type-Specific Issues
$article->tagsWithType('categories'); // Correct
$article->tagsWithType('category'); // Returns empty
Slug Conflicts
slugger in config/tags.php to avoid collisions:
'slugger' => function ($name) {
return Str::slug($name, '-', [' ']);
},
Custom Tag Models
Tag model to add fields (e.g., color):
class Tag extends \Spatie\Tags\Tag
{
protected $fillable = ['color'];
}
Event Listeners
TagCreated):
Tag::created(function ($tag) {
// Log or notify when a new tag is created
});
Custom Scopes
HasTags trait:
public function scopePopular($query)
{
return $query->withCount('taggables')->orderByDesc('taggables_count');
}
Validation Rules
public function rules()
{
return [
'tags' => 'required|array',
'tags.*' => 'string|max:50',
];
}
Caching
Cache::remember('popular-tags', now()->addHours(1), function () {
return Tag::withCount('taggables')->orderByDesc('taggables_count')->take(10)->get();
});
How can I help you explore Laravel packages today?