Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Tags Laravel Package

blamodex/laravel-tags

A lightweight Laravel package for adding tags to your Eloquent models. Attach, detach, and sync tags, query models by tag, and manage taggable relationships with a simple, expressive API that fits cleanly into any Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require blamodex/laravel-tags:^1.1
php artisan vendor:publish --provider="Blamodex\Tags\TagsServiceProvider"
php artisan migrate

Then, in order:

  1. Read the published migration first. It defines the tags table and the polymorphic pivot. That schema dictates almost every downstream decision (slug column, uniqueness, cascade behaviour), so it's the highest-value file to look at before writing a line of application code.
  2. Find the trait. The package ships a trait you add to any model you want taggable. This is the entire integration surface for 90% of use cases.
  3. Check the config file. Publish-time config usually controls the tag model class, table names, and slug generation — this is where you customise rather than fork.

First use case: a Post model with tags, an index page listing posts by tag slug, and a form field where editors type comma-separated tag names. Everything in the rest of this assessment is a refinement of that loop.

Method names below follow the package's documented trait API. Verify exact signatures against the README and src/ immediately after install — a tagging package's surface is small enough that it's worth ten minutes of reading before you build on it.

Implementation Patterns

Trait on the model, nothing else. Add the trait to Post, Article, whatever needs tagging. Polymorphic tagging means one tag vocabulary can be shared across content types, which is usually what you want — resist the urge to stand up a separate Category model alongside it until you actually need hierarchy.

Prefer sync over attach in write paths. attach-style helpers append and will happily create duplicate pivot rows; sync-style helpers replace the full set. In a form handler or an update action, sync is idempotent and matches user intent ("these are the tags now"). Reach for attach only for genuine append semantics, like adding a tag from a "related content" suggestion.

Normalise before you persist. Build one small service or form object that takes raw input and returns clean names:

$names = collect(explode(',', $request->input('tags')))
    ->map(fn ($t) => Str::of($t)->trim()->lower()->squish()->toString())
    ->filter()
    ->unique()
    ->values();

Do the dedupe and casing here, once. If you let Post and Tag write their own normalisation you'll end up with Laravel, laravel, and laravel as three rows within a month.

Wrap create-and-tag in a transaction. Creating a post and resolving/creating its tags are two writes. DB::transaction() around the pair prevents half-tagged records when a unique constraint fires mid-flight.

Query through the relationship, not the tag table. Filtering posts by tag is a whereHas on the tags relation, and tag pages are a where('slug', ...) on the tag model plus eager-loaded posts:

$posts = Post::whereHas('tags', fn ($q) => $q->where('slug', $tag->slug))
    ->with('tags')
    ->paginate(20);

Always eager-load tags for listings. A tag cloud or a post index renders tags per row; without with('tags') you get an N+1 per page. This is the single most common performance regression with any tagging package.

Route model binding by slug. Tags are a natural slug resource. Bind on slug rather than id and your URLs stay stable and readable. Keep the slug column indexed and unique.

API resources: expose tags via whenLoaded('tags') so clients opt in with ?include=tags. Return [{id, name, slug}] rather than raw models — pivot columns like created_at leaking into payloads is noise.

Seed a starter vocabulary. A seeder that creates your canonical tags up front means editors pick from existing rows instead of minting near-duplicates, and it keeps your tag index page non-empty during development.

Gotchas and Tips

v1.1.0 is a compatibility-only release. The changelog is a single line — Laravel 13 support — with no features, deprecations, or breaking changes announced. Bump to ^1.1 when you move to Laravel 13, and confirm your other packages support 13 before you pin it; a tagging package is rarely the blocker, but its dependencies can be. If you're staying on 10/11/12, there is no reason to rush this update.

Morph maps will bite you retroactively. If you call Relation::enforceMorphMap() (and you should), taggable_type starts storing aliases like post instead of App\Models\Post. Existing rows keep the old FQCN and silently stop matching. Write a data migration that rewrites taggable_type in the pivot in the same deploy as the morph map change.

Concurrent writes can still duplicate. Sync-then-insert isn't atomic across requests. Put a unique index on the tag name/slug and on the pivot (taggable_type, taggable_id, tag_id), and be prepared to catch a unique violation and retry — two editors tagging the same post simultaneously is a real scenario, not a theoretical one.

Deletes leave orphans unless the schema says otherwise. Check whether the published migration uses cascadeOnDelete on the pivot. If it doesn't, deleting a Post leaves pivot rows behind and your tag counts drift. Either amend the migration before first migrate (cheapest) or add cleanup in a model observer.

Case and whitespace are the whole game. Enforce normalisation at the input boundary and make the tag table's uniqueness case-insensitive if your DB supports it (MySQL's default collation does; Postgres needs a functional index). Otherwise the same tag appears twice with different casing depending on which code path created it.

Config caching. After publishing, changes to the config file do nothing until php artisan config:cache is re-run (or cleared in local dev). If a custom tag model "isn't being used," this is almost always why.

Debugging the pivot: php artisan tinker with $post->tags()->get() and DB::table('<pivot>')->where('taggable_id', $post->id)->get() side by side will tell you instantly whether the issue is the relationship definition or the underlying rows. Guessing at relationship config without looking at the rows wastes a lot of time.

Extension points, in order of preference:

  1. Config swap — point the config at your own Tag model extending the package's. Use this for extra columns or accessors.
  2. Published migration — add columns (colour, description, usage_count) before you first migrate.
  3. Overriding the trait method in your model — use sparingly; it's the hardest thing to reason about six months later.

Forking or vendoring the package is unnecessary for this class of library. If you find yourself wanting to, the requirement is probably a different feature rather than a patch.

Watch for: the tag-cloud/count query. withCount('posts') on every tag is fine at a few hundred tags and painful at tens of thousands. Cache the cloud and invalidate on tag write, or maintain a denormalised usage_count. You won't need it on day one, but know where the cliff is.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log