toro/tagging-bundle
Toro Tagging Bundle adds flexible tagging to Symfony applications: attach tags to any entity, query and filter by tag, and manage tags through a simple service and Doctrine integration. Includes taggable behavior, tag clouds, and helpers for building tag-based search and navigation.
Read this first: it is not a Laravel package. toro/tagging-bundle is a Symfony bundle — the Bundle suffix, the DependencyInjection/ + Resources/config/ layout, and Doctrine ORM entity mapping. There is no TaggingServiceProvider, no publishable config, no facade. composer require toro/tagging-bundle in a Laravel app will either fail dependency resolution or install classes that nothing autoloads into the container.
Second reality check: last release 2017-07-27, 0 stars, 0 dependents. It targets the Symfony 2.x/3.x + PHP 5.6/7.0 era. There is no issue triage, no security response path, and no one to ask when a Doctrine version bump breaks it.
If you still need to inspect it (e.g. you inherited a database written by it):
DependencyInjection/ and Resources/config/*.xml|yml to learn what services and parameters it registers.Entity/ (or Model/) directory for the Tag entity and the taggable interface/trait.The first use case it was designed for: a single polymorphic many-to-many tagging table attached to several entity types, with a manager service for find-or-create and query. The Laravel equivalent is ~20 minutes of work:
php artisan make:model Tag -m
php artisan make:migration create_taggables_table
Schema::create('tags', function (Blueprint $t) {
$t->id();
$t->string('name');
$t->string('slug')->unique();
$t->timestamps();
});
Schema::create('taggables', function (Blueprint $t) {
$t->foreignId('tag_id')->constrained()->cascadeOnDelete();
$t->morphs('taggable'); // taggable_id + taggable_type + index
$t->unique(['tag_id', 'taggable_id', 'taggable_type']);
});
That is the honest conclusion: start here, not with the bundle.
The patterns below are the bundle's concepts translated into Laravel idioms, which is how you'd actually leverage the idea day to day.
Trait on the taggable side. Mirror whatever the bundle's taggable interface promised:
trait HasTags
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable')->withTimestamps();
}
}
Tag resolution with find-or-create by slug. The bundle's manager service did this; in Laravel put it in a TagResolver action and normalize on write, not on read:
public function resolve(string $name): Tag
{
$slug = Str::slug($name);
return Tag::firstOrCreate(['slug' => $slug], ['name' => trim($name)]);
}
Syncing from a request. Translate a comma-separated tags input into IDs and sync():
$ids = collect(explode(',', $request->input('tags', '')))
->map(fn ($n) => trim($n))->filter()
->map(fn ($n) => $this->resolver->resolve($n)->id);
$post->tags()->sync($ids);
Querying — AND semantics. The bundle exposed manager queries; the Laravel version is whereHas plus a grouped having. This is the part people get wrong most often:
Post::whereHas('tags', fn ($q) => $q->whereIn('slug', $slugs), '>=', count($slugs))
->get();
Use '=' for exact-AND, '>=' when tags are hierarchical or you allow supersets.
Denormalized tag cloud. Keep a taggables_count on tags, incremented in an observer or a queued job. Recomputing withCount('posts') on every page render is the usual performance cliff.
Interop with an existing bundle-written schema. If you're migrating off a Symfony app that used this bundle, keep the bundle's exact table and column names in your new migration. Don't rename the pivot mid-flight, and don't add foreign keys that the old writer doesn't expect — it inserts directly and will start throwing on constraint violations during the overlap period.
Where to extend, if you fork. The only sensible extension points are the manager service (wrap it, don't edit it) and the taggable interface. Everything else is mapping, which you're better off re-expressing as Laravel migrations.
debug:container tip, every services.yml snippet you'll find for this package assumes Symfony. In Laravel, php artisan about and tinker tell you nothing about it. Confirm early that you're not chasing Symfony-shaped answers in a Laravel app.composer require may resolve to a Symfony 2.x/3.x doctrine chain or fail outright. If you must inspect it, clone it out-of-tree and read the source.sync() in Laravel is safe; raw inserts are not. The unique index on (tag_id, taggable_id, taggable_type) is not optional — without it, retries and parallel requests silently multiply rows and every count is wrong."Laravel", "laravel ", and "Laravel " produce three tags if you slug at read time. Slug and trim at write time, enforce unique on slug, and treat the display name as cosmetic.taggables rows unless you use cascadeOnDelete or a deleting observer. Tag clouds drift upward forever and nobody notices for months.$post->tags inside a loop is a query. Always with('tags') on index pages and withCount where you need totals.utf8mb4_unicode_ci your slug unique index is case-insensitive; on Postgres it isn't. A tag set that's deduplicated in dev can split in production if you switch drivers.bin/console debug:container toro_tagging and debug:doctrine to confirm what's actually registered and mapped. In a Laravel app there is nothing to debug — which is itself the answer.How can I help you explore Laravel packages today?