awes-io/repository
A Laravel repository pattern package that streamlines data access with reusable repositories, query filtering, sorting, pagination, eager loading, and CRUD helpers. It keeps controllers lean and models clean while making complex Eloquent queries easier to organize.
composer require awes-io/repository
namespace App\Repositories;
use AwesIO\Repository\Eloquent\BaseRepository;
use App\Models\Post;
class PostRepository extends BaseRepository
{
public function model(): string
{
return Post::class;
}
}
// AppServiceProvider::register()
$this->app->bind(PostRepository::class, PostRepository::class);
public function index(Request $request, PostRepository $posts)
{
return $posts
->pushCriteria(new RequestCriteria($request))
->with(['author'])
->paginate(20);
}
With this in place, GET /posts?status=published&sort=-created_at (exact syntax depends on the version) is translated into where/orderBy clauses without writing per-field controller code.Where to look first
vendor/awes-io/repository/src/Eloquent/BaseRepository.php — the methods you actually call (all, find, create, update, delete, paginate, pushCriteria, resetCriteria).vendor/awes-io/repository/src/Criteria/ — the built-in criteria, especially RequestCriteria, to confirm the request-parameter conventions your version supports.Thin controller, repository as the query surface. Keep the controller limited to HTTP concerns; let the repository own query construction. Anything more than a couple of where() calls belongs in a repository method or a criterion.
Criteria as composable filters. Each reusable filter is a class implementing the package's criteria contract with an apply($query) method:
class PublishedCriteria implements CriteriaInterface
{
public function apply($query)
{
return $query->where('status', 'published');
}
}
Then stack them per endpoint:
$posts->pushCriteria(new PublishedCriteria())
->pushCriteria(new ByCategoryCriteria($categoryId))
->paginate(20);
This is the sweet spot of the package: filters become small, individually testable units that compose cleanly across endpoints.
RequestCriteria + scopes for API filtering. The out-of-the-box request filtering is most maintainable when each filterable field is backed by a model scope or an allowlisted column. That gives you one place to validate, one place to rename API-facing keys to DB columns, and a guarantee that a new column in the table isn't suddenly filterable by a query parameter.
Domain methods on the repository. For filters used everywhere (->published(), ->forUser($user)), add methods to the repository rather than instantiating criteria at every call site. Reserve ad-hoc criteria for controller-specific filtering.
Service-layer orchestration for complex screens. When an endpoint needs filters + sorting + pagination + eager loading, wrap the composition in a small service or a dedicated "filter set" object, and have the controller call one method. It keeps the repository free of request/HTTP knowledge.
Testing. Because the repository is bound in the container, swap it with a stub or an in-memory fake in feature tests, and unit-test criteria against a Builder in isolation. Criteria that only take a Builder are trivial to assert with ->toSql() / ->getBindings().
Maintenance risk — verify compatibility first. Last release was mid-2021. Before adopting, install it against your actual Laravel/PHP version and run the test suite. The most common breakages are interface/return-type mismatches in the base repository contract (PHP 8 typed signatures, Laravel 9+ query builder changes). If signatures clash, you can often override the offending methods in your own base repository rather than forking.
RequestCriteria is a mass-assignment-style surface. Treat query parameters as untrusted input. Always restrict which fields/operators are accepted (allowlist in config or a dedicated $filterable array). Otherwise ?password=... or ?is_admin=1 becomes an enumeration oracle.
Repositories with pushCriteria are stateful. Calling pushCriteria mutates the instance. Under long-running workers (Octane, queue jobs) or when a repository is bound as a singleton, criteria leak across requests. Either bind as a transient (default for bind) or call resetCriteria() after execution — and prefer clone/fresh instances when composing in loops.
Empty and malformed filters. Guard against empty strings, empty arrays, and unknown sort columns. A blank ?status= silently adding where status = '' is a common source of "no results" bug reports; normalize inputs in a FormRequest before they reach RequestCriteria.
all() vs. paginate() return types differ. Downstream code (resources, tests) that assumes a Collection will break if the endpoint later switches to a paginator. Type-hint loosely or normalize in a response layer.
Relationship filtering needs explicit criteria. RequestCriteria typically maps to columns on the base table. Filters across relations (?author=..., ?tag=...) need a custom criterion using whereHas, or a scope that encapsulates it.
Debugging the final query. When a filter "doesn't work," dump the compiled query rather than guessing:
DB::listen(fn ($q) => logger($q->sql, $q->bindings));
or ->toSql() before execution. This quickly reveals criteria ordering problems and filters that are silently no-ops.
Order matters. Criteria applied after paginate() are ignored; criteria applied after with() can be fine, but criteria that call select() will drop eager-loaded columns. Establish a convention: criteria first, then eager loads, then pagination.
Extension points to know before forking. The base repository's applyCriteria, scopeQuery, and the model-resolution method are the intended seams. Overriding applyCriteria lets you add logging or query caching globally; overriding the model accessor lets you attach global scopes. Check these before patching vendor code — most needs are satisfiable by subclassing.
How can I help you explore Laravel packages today?