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

Eloquent Traits Laravel Package

ponich/eloquent-traits

A collection of reusable traits for Laravel Eloquent models that helps cut boilerplate by adding common model behaviors through simple trait imports, keeping your codebase cleaner and more consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer; there is no service provider or config file to publish — the package is a bag of traits you use inside your models.

composer require ponich/eloquent-traits

Where to look first: the src/ directory (or the README trait table). Each trait is a single, small concern — filtering, sorting, searching, defaults, nullables, booleans, slugs, UUIDs, and so on. Read the trait body before using it; most are 30–100 lines and that read is faster than guessing semantics.

First use case: an index endpoint on a resource. Instead of hand-rolling when($request->has('sort')) chains in the controller, put the traits on the model and declare which columns are allowed:

class Post extends Model
{
    use HasFilterable, HasSortable, HasSearchable;

    protected array $filterable = ['status', 'author_id'];
    protected array $sortable   = ['created_at', 'title'];
    protected array $searchable = ['title', 'body'];
}
$posts = Post::query()
    ->filter($request->query())
    ->search($request->string('q'))
    ->sort($request->string('sort'))
    ->paginate();

That is the day-to-day value: the model owns the whitelist, the controller stays dumb, and the scope name is consistent across every resource.

Implementation Patterns

Compose on a base model, not on every model. If most of your models are filterable/sortable, create an App\Models\Concerns\BaseModel (or an abstract Model) that uses the traits and extends Illuminate\Database\Eloquent\Model. That keeps trait order identical everywhere and gives you one place to override a scope that misbehaves.

Treat the declared arrays as a security boundary. The package traits generally read a $filterable / $sortable / $searchable property and silently ignore anything not listed — that is the whole point. Never derive those arrays from Schema::getColumnListing() or from request input; enumerate them.

Order the scope chain deliberately. Filter → search → sort → paginate. Filters narrow the row set, search adds a LIKE/FULLTEXT predicate, sort attaches ORDER BY. Reversing search and sort is harmless, but applying sort() before a filter that hits the same columns can confuse query-plan reading later.

Pair with a FormRequest for validation. The traits sanitise which columns are used, not the values. ?sort=-created_at,title and ?filter[status][in][]=draft should be validated for shape in a FormRequest so bad input fails with a 422 instead of a silently-empty result set.

Defaults and nullables belong in migrations too. If HasDefault populates attributes on instantiation and HasNullable converts empty strings to null, the database schema should agree — default(...) on columns and nullable() where appropriate. Traits are an application-layer guarantee, not a schema one; direct DB::table() inserts bypass them.

Slugs: wrap the trait's generation in a model event. Whatever the slug source column is, generate in saving (not creating) so updates to the source title produce an updated slug, and scope uniqueness to soft-deleted rows if you use SoftDeletes — otherwise a restored record can collide.

Boolean scopes are a naming convention, not magic to rely on. Verify in tinker that Post::whereIsPublished(true) exists after adding the boolean trait; if the column-to-scope mapping is derived from a property, it will not cover computed or relation-backed state.

Write one test per trait per model. These traits have no container bindings and no I/O, so a sqlite :memory: test asserting the generated SQL (or toSql()/getBindings()) is enough and is very cheap insurance against a package upgrade changing scope behaviour.

Gotchas and Tips

Low adoption is the main risk. 18 stars and essentially no dependents means no one else is stress-testing upgrades for you, and semver discipline in micro-packages is often informal. Pin an exact version in composer.json, and read the trait source when you bump it. The upside: MIT licence, tiny surface area — vendoring the two or three traits you actually use into app/Models/Concerns is a completely reasonable mitigation and takes minutes.

Trait method collisions are silent-ish. Laravel resolves boot{TraitName} automatically, and two traits defining scopeFilter will throw a fatal error only at class-load time — which in practice means your php artisan command breaks, not your web request. If you also use another query-builder trait (Spatie's QueryBuilder, for instance), audit for duplicate scopeFilter/scopeSort names before merging.

Sorting without a tiebreaker breaks pagination. ORDER BY created_at DESC alone lets rows shuffle between pages when timestamps tie. Always append the primary key: either include id in $sortable and document that clients should append it, or override the sort scope to append ->orderBy('id').

Case sensitivity and collation differ by driver. MySQL's default utf8mb4_unicode_ci makes LIKE '%foo%' case-insensitive; Postgres does not. A search that "works" locally on SQLite/MySQL can behave differently in production — test on the real driver, and consider ILIKE or a functional index if you need parity.

Searching relation columns invites N+1 or missing WHERE clauses. Most of these traits only search the model's own table. If you need author.name, use whereHas explicitly rather than adding author.name to $searchable and hoping it works — at best it errors, at worst it produces a cartesian product.

Filtering on indexed columns only. A filter[body][like] on a TEXT column is a table scan. Keep $filterable aligned with your indexes, and log slow queries (DB::listen) during development on the index endpoint specifically — it is the endpoint most likely to receive arbitrary combinations.

Defaults don't backfill. HasDefault applies to newly instantiated models. Adding a new defaulted attribute to an existing table needs a migration with ->default(...) and an update statement for existing rows; the trait will not retroactively fix them.

Debugging is just Eloquent debugging. Post::query()->filter([...])->toSql() and ->getBindings() will tell you in one line whether the trait ignored your parameter (empty WHERE) or applied it. If a filter "does nothing", the answer is almost always that the key isn't in $filterable, or the request shape doesn't match what the trait parses — check the trait's parsing code, not your controller.

Extension point is the model, not the package. Because these are traits, overriding a scope* method directly on the model wins over the trait's implementation. That is the clean way to customise behaviour; forking the package for a one-line change is almost never worth it.

Before adopting wholesale, compare with spatie/laravel-query-builder. If you need includes/relation filtering, sparse fieldsets, or a documented, widely-used query-string grammar, Spatie's package covers more ground. ponich/eloquent-traits is a better fit when you want a handful of independent, copy-pasteable concerns and no config file.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core