ingenious/eloquence
Eloquence extends Laravel's Eloquent ORM with ready-made traits such as Sluggable, Mutable, Mappable, Recursive, and Metable, adding slugs, custom casts, array access, and self-referencing relationships to your models with minimal boilerplate.
ingenious/eloquence is a monorepo of independent Eloquent traits rather than a single feature. You almost never want "Eloquence" — you want one or two of its parts: Sluggable, Sortable, Mappable, Searchable, Metable, Mutable, Recursive, Validable, plus a small QueryBuilder extension set.
Minimal install:
composer require ingenious/eloquence
The package was last released in September 2019, so it targets Laravel 5.x/6.x and PHP 7.x. Before adding it to an existing app, verify the framework constraint in composer.json resolves against your Laravel version. On Laravel 5.5+ the Eloquence\EloquenceServiceProvider is auto-discovered; on older versions add it to config/app.php manually. That provider mainly registers the QueryBuilder extensions — most traits work without any configuration at all.
Where to look first: the per-trait READMEs under src/ in the repository, and the test suite. Tests double as the most reliable documentation because the README lags behind the code in places.
First use case — a slug for a posts table:
// migration
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('slug')->unique();
$table->timestamps();
});
use Eloquence\Sluggable\SluggableTrait;
class Post extends Model
{
use SluggableTrait;
protected $fillable = ['title', 'slug'];
protected $sluggable = [
'build_from' => 'title',
'save_to' => 'slug',
'unique' => true,
];
}
$post = Post::create(['title' => 'Hello Eloquent World']);
$post->slug; // "hello-eloquent-world"
Post::whereSlug('hello-eloquent-world')->first();
Post::findBySlug('hello-eloquent-world');
That is the whole "hello world" — a saving hook builds the slug, uniqueness is handled for you, and a whereSlug / findBySlug helper is added to the model.
protected $sluggable = [
'build_from' => ['title', 'subtitle'], // concatenated, or a method name
'save_to' => 'slug',
'unique' => true,
'separator' => '-',
'max_length' => 120,
'include_trashed' => true, // uniqueness checks include soft-deleted rows
];
Useful in controllers: route-model binding via findBySlug, and bulk regeneration by calling $post->reslug() (or dispatching a queued job that loops and re-saves) after changing the source column. If you need slugs only for SEO and stable IDs for routing, keep both and route on the ID.
use Eloquence\Sortable\SortableTrait;
protected $sortable = ['position']; // column name, default "position"
// migration
$table->integer('position')->nullable()->index();
Day-to-day operations:
$item = Item::create([...]); // gets next available position
Item::sorted()->get(); // ordered by position
$item->moveToStart(); // or moveToEnd() / moveToPosition(3)
Item::findByPosition(2);
Typical pairing: a drag-and-drop admin list that PATCHes a single position and calls moveToPosition. Always wrap reordering in a transaction — reordering shifts every row between the old and new position.
use Eloquence\Mappable\MappableTrait;
protected $mappable = [
'name' => 'full_name', // model attribute => DB column
'email' => 'email_address',
];
Now $user->name reads full_name, $user->name = 'x' writes it, and User::whereName('x') maps the column in the query builder. This is the trait that saves real time when you inherit a database you cannot rename. Persist mapped attributes by listing them in $fillable exactly as the model attribute names.
LIKE search across columnsuse Eloquence\Searchable\SearchableTrait;
protected $searchable = ['title', 'body'];
// in a controller
Post::search($request->input('q'))->paginate(20);
Use this for admin back-office filtering over a handful of columns. For anything user-facing at scale, use Scout/Meilisearch/full-text indexes instead — see gotchas.
use Eloquence\Metable\MetableTrait;
$post->setMeta('og_image', '/img/cover.jpg');
$post->getMeta('og_image');
Needs a meta table holding metable_id, metable_type, key, value. Great for settings/SEO fields that vary per model type. Always eager load (->with('meta')) when listing, otherwise you get an N+1 per row.
use Eloquence\Recursive\RecursiveTrait;
protected $recursive = ['parent_id'];
Gives you parent, children, ancestors, descendants, siblings on a self-referencing table. Eager load with with('children') and, if you render a whole tree, load it flat and build the structure in PHP rather than recursing per node.
Sluggable + Sortable + Mappable on the same model is the common "CMS content" shape:
class Article extends Model
{
use SluggableTrait, SortableTrait, MappableTrait;
protected $sluggable = ['build_from' => 'title', 'save_to' => 'slug', 'unique' => true];
protected $sortable = ['position'];
protected $mappable = ['body' => 'content_html'];
}
$rules, $messages) when you want $model->save() to throw on invalid data, instead of scattering $request->validate() everywhere. Keep FormRequests for anything user-facing; use Validable for queued/imported writes.mutateFoo / accessFoo with a $mutable whitelist) when you have custom getters/setters you don't want mixed into the model body as getFooAttribute.where* helpers registered by the service provider) are worth skimming once — they're cheap wins for filter-heavy index screens.boot() callbacks.Version risk is the biggest one. Last release 2019-09-24 means no official Laravel 9/10/11 support. Check composer why-not laravel/framework before committing. If you must use it, pin the framework minor and treat the package as vendored-but-unmaintained: add tests around every trait you adopt.
Trait method collisions. MappableTrait overrides core attribute access (getAttribute / setAttribute / getAttributeValue). If you also use a trait that touches those, or your own getFooAttribute magic, resolve conflicts explicitly:
use MappableTrait, OtherTrait {
MappableTrait::getAttribute insteadof OtherTrait;
OtherTrait::getAttribute as otherGetAttribute;
}
Trait order in the use line matters here — don't rely on luck.
Slug uniqueness is not concurrency-safe. Two simultaneous creates with the same title can both pass the uniqueness check before either inserts. Keep the DB unique index and catch the QueryException (or retry with a suffix). Also watch max_length — long titles truncated at different points can collide.
include_trashed surprises people. If you soft-delete a post, its slug is still "taken" unless you set this. Decide deliberately; changing it later leaves orphaned duplicates.
Sortable positions drift. Deleting a row leaves a gap; nothing renumbers automatically. Either accept gaps (only relative order matters) or add a resequence() routine you call after deletes. Under concurrent writes, nextPosition() can race — wrap create-and-position in a transaction, or lockForUpdate() on the table tail if your DB supports it.
Mappable only maps where it's wired in. Builder methods and relationship constraints mostly map; raw expressions, selectRaw, orderByRaw, and hand-written SQL do not. Debugging a "why is this column unknown" error almost always means a raw fragment used the model attribute name instead of the DB column. Dump $model->getMappedAttributes() (or just read $mappable) when in doubt.
Searchable is LIKE %term%. It cannot use a normal B-tree index, so it degrades linearly. It's fine for a few thousand rows and an admin filter; it is not a replacement for Scout. If you inherit a screen that's already slow, the fix is usually a full-text index (WHERE MATCH ... AGAINST) or moving to Scout — not tuning this trait.
Metable needs eager loading and a serialization decision. $model->toArray() will include meta if loaded, which can bloat API payloads. Decide whether meta is exposed, and if not, $hidden = ['meta'] plus explicit accessors.
Recursive trees are N+1 factories. $node->descendants walks the tree with queries per level. Recursive eager loading (with('children.children.children')) is bounded and brittle; loading all rows once and assembling in PHP scales better. For very large, frequently-moved trees, a nested-set implementation is the right tool — this trait is adjacency-list only.
Validation trait throws — make sure that's what you want. Validable failing inside an event listener or a queued job can look like a mysterious failure. Catch ValidationException at the job boundary and log context.
Extension points. Every trait is designed to be subclassed or overridden: buildSlug() for custom slug formatting, the whereSlug scope for custom lookup semantics, getMeta for defaulted metadata. Override the small method rather than copying the trait into your app — that keeps upstream fixes (such as they are) reachable.
Debugging checklist: ->toSql() and ->getBindings() on any surprising query; check whether the trait hooks into saving/creating (they do, so saveQuietly(), insert(), and raw updates bypass them); and confirm the service provider is registered if builder helpers appear to be missing.
How can I help you explore Laravel packages today?