myerscode/laravel-query-strategies
myerscode/laravel-query-strategies provides reusable, composable query strategies for Laravel Eloquent models, helping you encapsulate filtering, sorting, and search logic to keep controllers clean and queries testable.
This release is a large feature drop for myerscode/laravel-query-strategies, turning it from a basic query-parameter handler into a fairly complete query-strategy layer for Laravel APIs. It targets Laravel 13 / Testbench 11, so treat 13.0.0 as a hard version bump alongside the feature work.
Minimal path:
composer require myerscode/laravel-query-strategies and confirm your app is on Laravel 13. If you are still on Laravel 11/12, stay on the previous major.php artisan vendor:publish for the package config) and skim it before touching code — strict mode, defaults, and strategy resolution all live there.First use case: an index endpoint such as GET /api/posts?filter[status]=published&sort=-created_at&include=author,comments&fields[posts]=id,title. That single request exercises filtering, sorting, relationship includes, and field selection — the core of the new feature set. Start with filters + sorting only, then layer in includes and field selection once the basics round-trip.
Where to look first: the strategy base class (to see defaultMethods and how parameters map to clauses), the clause classes under the filter namespace, and the config file. The clause classes are the best documentation of what query-string syntax the package actually accepts.
One strategy per model, with model auto-pickup. Rather than wiring every controller manually, define a strategy class per Eloquent model and let the new default-model-strategy / auto-pickup behaviour resolve it. This keeps the allowed-filter surface next to the model it protects and removes repetitive controller plumbing.
Compose filters from clauses, not closures. The release adds BetweenClause, IsNull/IsNotNull, TrashedClause, and ScopeClause. Prefer these over ad-hoc logic:
filter[created_at][between]=2024-01-01,2024-12-31filter[deleted_at][is_null]=1TrashedClause for withTrashed/onlyTrashed toggles on models using SoftDeletesScopeClause to expose an existing scopePublished() etc. as a query parameter — this is the cleanest way to reuse business logic you already have.Relationship filtering and nested sorting. Filters and sorts can now traverse relations (e.g. sort=author.name, filter[author.status]=active). Use this instead of hand-rolled whereHas in controllers, but keep an eye on the generated SQL for deep paths.
Aggregate relationship includes. For list screens that show counts (comment counts, like counts), use the aggregate include support rather than loading full relations and counting in PHP.
Field selection (sparse fieldsets). fields[posts]=id,title lets clients trim payloads. Pair this with append support for accessors so computed attributes (full_name, url) are only serialized when explicitly requested.
Callback / inline filters. For the one-off case that does not deserve a clause class, register a closure filter. Keep these few — they are invisible to static analysis and easy to duplicate.
Defaults and ignore lists. Use default filter values so an omitted parameter resolves to a sane baseline, and the ignored-values list so sentinel values like '', all, or null are treated as "no filter" rather than literal matches. This is the difference between a filter that behaves well in a UI and one that returns zero rows.
Opt-in strict mode. Enable strict mode once your strategies are stable. It converts unknown/undeclared parameters from silent no-ops into hard failures — invaluable during client development, mildly hostile in production if clients send experimental params. Roll it out per-environment first.
Shorthand clause mapping. Parameters can reference a clause class via a shorthand instead of the fully-qualified name. Use it in user-facing docs and client SDKs; keep the FQCN in code where clarity matters.
Parameter is now readonly. Any code that mutated a parameter object in place will break. Construct a new instance instead. If you have custom clause implementations, audit them for property writes.StrategyManager no longer double-instantiates strategies. If you relied on constructor side effects firing twice (or worked around them), remove the workaround. Strategy constructors should be side-effect free anyway.defaultMethods map is cached. The compiled map is memoised for performance. If you mutate strategy definitions at runtime (feature flags, tenant-specific filters), clear or bypass the cache explicitly — otherwise your changes will not take effect on subsequent requests in the same process.limit() before paginate() was removed. If your strategy added a limit for "safety", pagination totals and page counts may shift. Verify total() and last-page behaviour after upgrading.include=author where author is not a real relation will fail instead of silently doing nothing. That is better, but it can surface latent typos in existing clients — roll out with logging first.SoftDeletes. TrashedClause is meaningless on a model without the trait; expect either an exception or a silently ignored parameter depending on strict mode.id or a foreign key, either append it server-side or document that clients must request it. Missing keys produce confusing null attributes rather than clear errors.EXPLAIN on large tables and add indexes on the relation's join columns before exposing these to clients.->toSql() / ->getBindings(), or DB::listen) for a representative request before and after upgrading. This is the fastest way to spot the removed-limit and changed-relation-validation behaviour.How can I help you explore Laravel packages today?