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

Eloquence Laravel Package

dmn013/eloquence

Eloquence extends Laravel's Eloquent ORM with a suite of opt-in traits: fuzzy searchable models, sortable and sluggable records, soft deletes, cacheable queries, mappable attributes, and more. Drop-in behaviors that keep your models clean and your code expressive.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require dmn013/eloquence

Before writing any code, do two things:

  1. Check the version constraint against your framework. This is a 2019-era package (upstream jarektkaczyk/eloquence, last release 2019-09-24). Run composer require dmn013/eloquence -W and look at what it pulls in; if your app is on Laravel 10/11, expect the constraint to fail or the traits to break against modern HasAttributes/Builder internals. On anything newer than Laravel 6, plan to vendor the traits you use into app/Support/ rather than depend on the package.
  2. Read vendor/dmn013/eloquence/src/ first, not the README. The README is accurate for the 2019 release but namespaces moved between versions (Eloquence\SluggableTrait in older releases vs. grouped Eloquence\Behaviours\* traits later). The trait files are the source of truth for method names and property keys.

Eloquence is not one feature — it is a bundle of independent traits. You opt into each one per model:

use Eloquence\SluggableTrait;

class Post extends Model
{
    use SluggableTrait;

    protected $sluggable = ['slug' => 'title'];
}

The first, highest-value use case is almost always Sluggable, because it kills a whole class of boilerplate (observer + uniqueness check + findBySlug):

$post = Post::create(['title' => 'Hello Eloquent World']);
$post->slug;                                   // "hello-eloquent-world"
Post::findBySlug('hello-eloquent-world');      // model instance or null

Route binding then becomes trivial:

Route::get('/posts/{slug}', fn ($slug) => Post::findBySlugOrFail($slug));

Implementation Patterns

Pick traits per model rather than applying the bundle everywhere — several of them override getAttribute/setAttribute/save, so the blast radius is real.

Sluggable — SEO routes and permalinks. Declare the source column(s) in $sluggable. For content that gets renamed, decide explicitly whether slugs are immutable (better for SEO, links never rot) or regenerated. Uniqueness is handled by appending -1, -2, but you should still have a unique index on the column as the real guard against concurrent inserts. Pair it with Recursive for category trees so /category/parent/child resolves off slugs at every level.

Recursive — category trees, comment threads, menu structures. Set the parent column and get parent(), children(), ancestors(), descendants(), isRoot(), plus reparenting helpers. This is adjacency-list, not nested set — cheap writes, recursive reads:

protected $parentColumn = 'parent_id';

$category->makeChildOf($parent);   // reparent
$category->descendants;            // whole subtree

Use it for depth ≤ 4 or so. Always eager load with constraints (with('children.children')) and index parent_id; a naive tree render will happily issue one query per node.

Metable — optional attributes without migrations. Attaches a polymorphic key/value store to the model. Excellent for per-post SEO overrides, per-tenant settings, feature flags on a record — anything where only 10% of rows use the field:

$post->meta->seo_title = 'Custom title';
$post->save();                     // persists meta alongside the model
$post->meta->seo_title;            // retrieve
$post->meta()->where('key', 'x');  // query the store

The trade-off is that meta is not a column: filtering, sorting, and indexing on meta values requires a join you write yourself, and each model needs the relation eager loaded (with('meta')) to avoid an extra query per row.

Mutable — attribute coercion in one place. Declares per-attribute types so you stop scattering getXAttribute/setXAttribute pairs across a model. Good for JSON-ish columns and typed scalars; it composes naturally with Metable for structured blobs.

Searchable — admin quick-search, not product search. A $searchable = ['title', 'body'] property plus a search($term) scope that ORs LIKE clauses. That is exactly right for a back-office "find this record" box and exactly wrong for anything user-facing with relevance ranking — reach for Scout/Meilisearch there. Keep it off unindexed large text columns.

QueryBuilder / Cacheable — plumbing. The query helpers are thin sugar over common where shapes; the cache trait wraps a read query in a cache key and invalidates on model save/delete. Use the cache trait only on read-heavy, write-rare endpoints, and never on queries you also mutate with Post::where(...)->update() — bulk operations bypass model events, so the cache and slugs both go stale.

Validating lets a model declare $rules and reject its own invalid saves. Convenient in seeders and admin forms; in request-facing code you still want FormRequests so errors surface at the HTTP layer.

Gotchas and Tips

  • Trait collisions are the number-one failure mode. Many of these traits override getAttribute, setAttribute, or save. If you also have your own accessors, a HasAttributes override from another package, or two Eloquence traits touching the same hooks, method resolution order silently determines behavior. Add them one at a time and run the model test suite after each.
  • Mass assignment and fill() may not trigger your slug. Slugs are produced on save; if a column is not in $fillable the source value may be missing at build time. Verify with a create, an update, and an updateOrCreate.
  • ::update() and ::insert() bypass everything. Slugs, meta, validation, cache invalidation — all of it. Any code path that writes through the query builder needs a manual follow-up.
  • Recursive has no cycle protection. $a->makeChildOf($b) where $b is already a descendant of $a produces an orphaned loop and descendants() will not terminate cleanly. Validate parentage in your form request.
  • Slugs and soft deletes interact. A trashed row still occupies the slug; decide whether uniqueness scopes to deleted_at. Concurrent inserts can both pass the "is it taken?" check — the unique index is what actually saves you.
  • Debugging: wrap DB::listen(fn ($q) => logger($q->sql)) around a single page render; you will immediately see whether Metable/Recursive are N+1'ing. Eloquence's magic is invisible in the code, so the query log is your only reliable view.
  • Abandonment is the real risk, not a bug. With no release since 2019 and zero dependents on this fork, the sensible long-term move is to copy the two or three traits you actually use into your own namespace (each is a few hundred lines) and drop the dependency. That also lets you rewrite the parts that poke at Eloquent internals and break on new framework versions.
  • Testing: use RefreshDatabase, point the cache driver at array in phpunit.xml, and assert on slugs explicitly — a green suite does not prove the slug hook fired if you never read the column.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin