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

Easy Model Laravel Package

ramadan/easy-model

ramadan/easy-model is a Laravel package that simplifies Eloquent model development with convenient helpers and utilities for common model tasks, reducing boilerplate and helping you build clean, maintainable applications faster.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer and let Laravel's package discovery register the service provider:

composer require ramadan/easy-model

Before writing any code, do these three things in order:

  1. Read the README, then the test suite. As of v2.1.0 the package ships an Orchestra Testbench + PHPUnit suite — that is now the most reliable documentation of intended behaviour. tests/ will show you the exact call signatures for fromRequest(), the allowed*() allow-lists, and what the pagination and mass-write methods return. Trust the tests over the README where they disagree.
  2. Check the version constraints and the changelog diff. Pin "ramadan/easy-model": "^2.1". If you were on 2.0.x, read the 2.0.0...2.1.0 compare: v2.1.0 adds a large surface (request-driven queries, soft deletes, mass writes) but the release notes don't flag removals — still, scan for method renames before assuming drop-in compatibility.
  3. Note there is no config file mentioned in the release notes. allowedFilters(), allowedSorts(), allowedIncludes(), and allowedSearch() are configured per query at the call site, not via published config. That's the package's central design decision and it changes how you organise code (see below).

First use case: take one controller action with a handful of conditional where() calls plus a search filter, and rewrite it as a declared, request-driven query:

$users = User::query()
    ->fromRequest($request)
    ->allowedFilters(['status', 'role'])
    ->allowedSorts(['created_at', 'name'])
    ->allowedIncludes(['team'])
    ->allowedSearch(['name', 'email'])
    ->paginate();

Compare ->toSql() and ->getBindings() before and after — the package exposes toSql() itself, so this costs nothing. If the SQL matches what your hand-written method produced, you've validated the abstraction on a low-risk query. Start with the most-repeated query in your codebase, not the most complex one.

Implementation Patterns

Make the allow-list the contract. The allowed*() methods are the idiomatic entry point and they replace the hand-rolled filter-array pattern the package previously implied. Define the allowed keys once per resource, close to the controller or in a query object, and treat that list as your API surface:

// app/Queries/UsersIndexQuery.php
final class UsersIndexQuery
{
    public function __construct(private Request $request) {}

    public function build()
    {
        return User::query()
            ->fromRequest($this->request)
            ->allowedFilters(['status', 'role', 'created_at'])
            ->allowedSorts(['created_at', 'name'])
            ->allowedIncludes(['team', 'posts'])
            ->allowedSearch(['name', 'email', 'posts.title'])
            ->when($this->request->boolean('trashed'), fn ($q) => $q->onlyTrashed());
    }
}

This keeps controllers thin and gives you one place to edit when the package's API shifts. The when()/unless() helpers added in v2.1.0 are the right tool for these opt-in modifiers.

Validate values, not just keys. allowedFilters(['status']) restricts which columns can be filtered; it does not restrict what values arrive. Keep a FormRequest in front of it ('status' => ['nullable', Rule::in(['active','pending','banned'])], 'sort' => ['nullable', 'regex:/^-?[a-z_]+$/']) so enum-like and free-text filters can't be used to probe or produce pathological queries. The allow-list is a blast-radius control, not input validation.

Keep the standard Laravel chain. Controller → FormRequest → Query object → API Resource still applies; the package replaces the query construction in the middle, not the edges. Pagination methods return paginators, so JsonResource::collection() continues to emit meta/links — verify the concrete return type (LengthAwarePaginator vs CursorPaginator) for whichever of paginate(), simplePaginate(), or cursorPaginate() you pick, because the JSON shape differs.

Separate read paths from write paths. v2.1.0 adds performInsert(), performUpsert(), restore(), forceDelete(), and onlyTrashed() — the package is no longer read-only. Resist using mass-write methods inside query objects; keep them in dedicated Action classes so an accidental forceDelete() on a wide filter is obvious in review and easy to wrap in a transaction.

Compose with scopes and macros. Eloquent local scopes and Builder::macro() still work alongside package methods. Use scopes for domain rules (scopePublished) and reserve package methods for generic filter/sort/include/paginate behaviour. That keeps a future removal mechanical rather than a rewrite.

Cache at the query-object level. Include the whole allow-listed filter/sort/include payload in the cache key. With fromRequest() in play, the request itself is the input — hash a normalised array of just the allowed keys, not $request->all(), or a stray tracking parameter will fragment your cache into uselessness.

Gotchas and Tips

  • usingModelEvents() is opt-in for a reason. performInsert() and performUpsert() are bulk operations; without usingModelEvents() they skip observers, model events, and anything your app relies on (audit logs, cache invalidation, UUID generation, saved hooks). If you have observers on the model, call usingModelEvents() or you will silently lose them. Test one insert with an observer assertion before trusting it.
  • forceDelete() on a query wrapper is a loaded gun. It takes a builder, not a model, so a mis-scoped filter deletes more than intended. Wrap every restore()/forceDelete() call in a DB transaction, assert the affected count, and gate it behind a policy check at the controller — soft-delete scoping is not an authorisation layer.
  • Dotted relation columns imply joins. allowedSearch(['posts.title']) and the v2.1.0 relation-join aliasing in addOrderBy() mean the package is joining tables for you. Two consequences: (a) you can get duplicate parent rows when a parent has multiple matching children — check whether you need distinct (and whether the package exposes it) or whether whereHas would have been safer; (b) joins can silently conflict with your own ->join() calls in the same query. Inspect toSql() whenever search and sort touch relations simultaneously.
  • toSql() is now first-class — use it as your debugging tool. Log ->toSql() and ->getBindings() after each package call in development. Binding-order bugs in filter and search mappers are the classic failure mode: they're invisible until a row exists where the wrong parameter happens to match.
  • JSON, date, and period filters are database-specific. addWhereJsonContains(), addWhereDate(), and addWherePeriod() will emit JSON_CONTAINS on MySQL, @> on Postgres, and json_extract on SQLite. If CI runs SQLite and production runs Postgres, add at least one integration test against the production engine, or you'll ship a filter that works in CI and 500s in production.
  • allowedIncludes() is an eager-loading allow-list — verify it holds. After the package methods run, inspect $query->getEagerLoads() and confirm your addWith()/addWithCount() calls and the allow-listed includes are all present. Custom builders that don't proxy everything through __call can drop them.
  • Watch for eager execution. Some helpers (paginate(), simplePaginate(), cursorPaginate(), and the mass-write methods) execute immediately and return results, not a builder. If you get a paginator back where you expected a builder, you can't chain further conditions — reorder so all when()/unless() modifiers are applied before the terminal call.
  • Verify eager loading survives the escape hatch. Confirm you can still reach the underlying Illuminate\Database\Eloquent\Builder (typically ->getQuery() or ->toBase()). With soft-delete and JSON filtering added, the chance of hitting a missing feature grows; no escape hatch means a fork.
  • Test the empty-request case explicitly. fromRequest() with no query string, and allowedFilters([]), are the classic "all rows returned" or "WHERE status = NULL" bugs. Assert that an unset filter means no constraint, not a null comparison.
  • Still treat it as an unthinned dependency. The new Testbench suite is a genuine maturity signal, but the project remains low-star with no dependents. Run composer audit, grep the source for DB::raw() with interpolated input, and keep everything behind your own query-object layer so the package stays replaceable.
  • Clear caches in production after upgrading. php artisan config:clear and composer dump-autoload — package-discovered providers and facades are cached in bootstrap/cache/, and a stale cached provider after a feature-heavy minor bump produces confusing "method does not exist" errors.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo