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

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

Sluggable — build slugs from any column combination

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.

Sortable — ordered lists without hand-rolled position math

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.

Mappable — legacy or awkwardly-named columns

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.

Searchable — fast LIKE search across columns

use 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.

Metable — key/value attributes without schema churn

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.

Recursive — parent/child trees

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.

Combining traits on one model

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'];
}

Integration tips

  • Put validation next to the model with the Validable trait ($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.
  • Use Mutable (mutateFoo / accessFoo with a $mutable whitelist) when you have custom getters/setters you don't want mixed into the model body as getFooAttribute.
  • The QueryBuilder extensions (extra where* helpers registered by the service provider) are worth skimming once — they're cheap wins for filter-heavy index screens.
  • Prefer these traits for model-level invariants (a slug always exists, order is always contiguous) and keep controller code thin. That's the package's actual value proposition: fewer bespoke boot() callbacks.

Gotchas and Tips

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.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
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