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

rinvex/laravel-tags

Rinvex Laravel Tags adds flexible tagging to Eloquent models, letting you attach tags and tag types to any model polymorphically. Manage tags, slugs, and relationships easily with a clean API for building searchable, categorized content in Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish. The package ships migrations (tags, taggables and — because Tag is translatable — a translations table) plus a config file:

composer require rinvex/laravel-tags

php artisan vendor:publish --provider="Rinvex\Tags\Providers\TagsServiceProvider"
php artisan migrate

Run php artisan vendor:publish with no arguments once and read the prompt list; the tag groups differ from other Rinvex packages and are not documented well in the repo.

2. Attach the trait to anything taggable.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Rinvex\Tags\Traits\Taggable;

class Post extends Model
{
    use Taggable;
}

That gives you a tags() polymorphic relation and the attach/detach helpers. Nothing else to wire up — no interface to implement, no service provider registration in your app.

3. First use case: a taggable blog post + a tag archive page. This is the 20-minute win that tells you whether the package fits:

$post = Post::create($request->validated());
$post->attachTags(['laravel', 'eloquent']);

// Archive page
Route::get('/tags/{tag:slug}', function (Tag $tag) {
    return view('tags.show', [
        'tag'   => $tag,
        'posts' => Post::whereHas('tags', fn ($q) => $q->where('id', $tag->id))
            ->latest()->paginate(20),
    ]);
});

Where to look first: src/Traits/Taggable.php (the entire public API lives there — roughly a screen of code), then src/Models/Tag.php for the translatable/sluggable setup, then the published migrations to confirm table and column names. The README is thin and the repo is archived, so the source is the documentation.

Implementation Patterns

Normalize input before it hits the package. Tags come from free-text fields, and the package will happily create Laravel, laravel and Laravel! as three rows whose slugs collide. Own the normalization at the edge:

private function normalizeTags(?string $raw): array
{
    return collect(explode(',', (string) $raw))
        ->map(fn ($tag) => trim($tag))
        ->filter()
        ->unique(fn ($tag) => Str::slug($tag))   // dedupe on slug, not raw string
        ->values()
        ->all();
}

Create vs. update. attachTags() for a create, setTags() (full replacement) for an update form — that's the pattern that keeps a comma-separated input field declarative and idempotent:

public function update(UpdatePostRequest $request, Post $post)
{
    $post->update($request->validated());
    $post->setTags($this->normalizeTags($request->input('tags')));

    return back();
}

If setTags isn't present in your installed version, compose it from detachTags($post->tags->pluck('id')) + attachTags($names) inside a transaction.

Resolve tags by slug, not name. Slugs are the stable public identifier (they're in URLs), and they're what the translatable name collapses to. Build helper scopes rather than repeating whereHas everywhere:

// Post model
public function scopeWithAnyTag(Builder $query, array $slugs): Builder
{
    return $query->whereHas('tags', fn ($q) => $q->whereIn('slug', $slugs));
}

Tag cloud / sidebar counts. withCount on the inverse relation is the cheapest way to get usage numbers; add the inverse relation yourself on a custom Tag model (see below), since Tag::taggables() is not guaranteed to exist:

$tags = Tag::has('taggables')
    ->withCount('taggables')
    ->orderByDesc('taggables_count')
    ->take(50)
    ->get();

Custom Tag model via config. Don't edit vendor/. Publish the config, point it at a subclass, and add the columns/scopes you actually need (color, description, is_featured):

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\MorphMany;
use Rinvex\Tags\Models\Tag as BaseTag;

class Tag extends BaseTag
{
    protected $fillable = ['name', 'slug', 'color'];

    public function taggables(): MorphMany
    {
        return $this->morphMany(\Rinvex\Tags\Models\Taggable::class, 'taggable');
    }
}
// config/rinvex.tags.php
'models' => [
    'tag' => \App\Models\Tag::class,
],

Eager load translations and tags together. The Tag name is translatable, so every un-eager-loaded tag in a listing is a query:

Post::with(['tags.translations'])->paginate();

Wire it into an admin form / API resource. Treat the tag list as a single comma-separated (or array) field in your FormRequest ('tags' => ['nullable', 'string', 'max:255']), keep normalization in one place (a small TagInput action or a custom cast), and return $post->tags->pluck('name') from your API resource so clients round-trip the same shape they submitted.

Backfill existing data. Because the package is a thin polymorphic layer, a one-off artisan command looping over records with attachTags() in chunks of 500 is the standard migration path off a legacy post_tags join table.

Gotchas and Tips

The repo is archived — treat it as vendored code, not a dependency. With 29 stars and zero dependents, there's no community to fix things and no Laravel 11/12 compatibility work coming. Before adopting, run composer require and see whether the rinvex/laravel-support constraint resolves against your framework version. If it doesn't, fork it into packages/ with a path repository (it's a few hundred lines) rather than fighting constraints with --ignore-platform-reqs.

Morph map is a one-way door. If you call Relation::enforceMorphMap() later, every existing taggable_type value in the pivot table is a raw FQCN and silently stops matching. Decide on the morph map before the first tag is written, or write a data migration alongside the change.

Orphaned pivot rows are real. There is no guaranteed cascadeOnDelete on the polymorphic pivot, and setTags()/detachTags() never garbage-collect Tag rows that no longer have any taggables. Add a scheduled cleanup:

Tag::doesntHave('taggables')->where('created_at', '<', now()->subDays(30))->delete();

And make sure deleting a Post detaches its rows — a deleting observer calling $post->tags()->detach() is cheap insurance against taggable_type rows pointing at IDs that now belong to a different model.

Slug collisions throw instead of merging. The Sluggable behaviour generates the slug from the (translated) name, so Laravel, laravel, and Laravel all want laravel. Concurrent form submissions will hit the unique index. Wrap tag creation in firstOrCreate(['slug' => Str::slug($name)], ['name' => $name]) at the boundary, and don't rely on the package to dedupe by name.

Locale-aware slugs. With translations on, the slug is derived per locale. If you later add de and the German name is identical to the English one, you can get a unique-violation on the translations table, not the tags table — the error message points at the wrong place. Seed translations deliberately and check the *_translations migration for which unique indexes actually exist.

Index the pivot manually. Verify the published migration indexes (taggable_type, taggable_id) and tag_id. Tag archive pages do whereHas on every request; without those, it's a full scan on taggables that only shows up in production.

Config caching. rinvex.tags.* values (models, table names) are read at runtime — if you override a model class, run php artisan config:clear before debugging "my custom Tag model is ignored."

Debugging a missing relation. When $post->tags returns empty but the pivot rows exist, the cause is almost always a mismatch between the taggable_type stored and the class now being resolved (renamed model, namespace move, morph map). Query taggables directly with DB::table('taggables')->where('taggable_type', $post->getMorphClass()) to confirm.

Testing. No factories ship with the package. Write your own TagFactory and make sure your test suite runs the published migrations; also pin a test that asserts the slug for a mixed-case name, since that's the invariant most likely to break if you fork.

Extension points worth knowing: custom Tag model via config (shown above), Eloquent model events on Tag (created/deleted) for cache invalidation, and Taggable's relation as the seam if you ever need to swap in spatie/laravel-tags — the attach/detach call sites are usually thin enough that a migration path exists if the archived status becomes a blocker.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle