mmanos/laravel-taggable
mmanos/laravel-taggable is a Laravel package for adding reusable tags to Eloquent models. Its taggable trait provides polymorphic tag relationships and helpers to tag, untag, retag, and query models by tag, plus scopes for filtering, making tag management simple across your application.
The package ships a custom Eloquent query builder that overrides the parent builder's first() method so it resolves from a collection instead of issuing a fresh SELECT ... LIMIT 1. In practice that means the "first record" can come from data you already hydrated (a get() result, a cached payload, an eager-loaded relation set) rather than a new round-trip to the database.
Minimal steps to get productive:
composer require <vendor>/<package> — check the resolved version. v1.0.1 is the release that introduces the first() override, so anything pinned at 1.0.0 will behave differently.Illuminate\Database\Eloquent\Builder and reimplementing first(). That single method is the entire surface area of this release; read it before anything else.newEloquentBuilder() on the model, a trait, or a base model the package expects you to extend. If there is a config file or service provider, publish it and confirm whether the behaviour is global or per-model.first() test in the package's suite that demonstrates the intended collection source. That test is your de-facto spec.First use case to try: a read endpoint that already does Model::where(...)->get() for a list view and then calls ->first() on the same result. In v1.0.1 that second call should be free — verify it with DB::listen() and count the queries.
Load-once-then-first. The intended shape is: build the query, get() the collection, and derive the head of that collection rather than issuing a second query. Good fit for dashboards, "latest item" widgets, or any screen where the list and the highlighted record are the same data set.
Repository / cache layer. If you already return an Eloquent Collection from a cache or an in-memory store, ->first() becomes the natural accessor and stops being the one line in your repository that escapes the cache and hits the DB. This is where the override earns its keep.
Query-count assertions. Bake the guarantee into tests rather than trusting it:
DB::enableQueryLog();
$result = $query->get()->first();
$this->assertCount(1, DB::getQueryLog()); // only the get()
Keep the two paths separate. Have hot paths that need a collection-derived first explicitly go through the package builder, and leave first()-as-a-query on models/query builders that don't opt in. Blurring the two makes the next debugging session expensive.
Transformers and API resources. Collections handed to Resource::collection() plus a separate ->first() for a meta block is a clean pairing — one hydration, two consumers.
Scope the override. If the package lets you register the builder per model, apply it only to models whose read paths genuinely reuse collections. A global override turns every first() in the app into a semantic question mark.
firstOrFail() / sole() / value() interactions. These typically delegate to first() internally. If the override returns a collection-derived model, firstOrFail() may throw ModelNotFoundException for a row that exists in the database but wasn't in the loaded collection. Audit every derived method, not just first().->where('active', 1)->first() will not filter the collection unless the override re-applies constraints. Filtering done on the builder after the collection exists is a no-op — and a silent one.orderBy('created_at', 'desc')->first() returns "first in the loaded collection," which is whatever order get() produced. If you need DB-ordered head, sort the collection or don't use this path.first($columns) ignores the column list in most collection-backed implementations, since the models are already fully hydrated. Don't rely on partial selects for memory savings here.first() that used to see fresh rows will now see cached ones. Pair with refresh()/load() where correctness matters.get() time. first() doesn't fix N+1; loadMissing() still does. Keep Model::preventLazyLoading() on in non-production.1.0.0 → 1.0.1) that changes query behaviour. Treat it as behavioural, not cosmetic: pin 1.0.1 explicitly, and if you have query-count or assertDatabaseCount-style tests, run them before deploying.toSql() and getBindings() describe the underlying query, not what the override actually does. Reach for DB::listen() or DB::enableQueryLog() to confirm whether a query fired at all; add dump($builder->getQuery()) when you suspect constraints were discarded.firstOrFail, sole, or re-apply pending wheres against the collection. Overriding newEloquentBuilder() on your own base model is the least invasive hook. Macros registered on the builder are unaffected by the override but can shadow it if named first.find(), firstWhere(), cursor(), and pagination are untouched — don't assume the collection-first behaviour generalises across the builder API.How can I help you explore Laravel packages today?