salehhashemi/laravel-repository
A Laravel package implementing the repository pattern with a clean abstraction layer for database access. It adds dynamic filtering, reusable criteria, custom search logic, and a make:repository command to scaffold repositories, interfaces, and filters.
Install and check compatibility first. The package requires PHP 8.2+ and declares support for Laravel 12.x/13.x only (v0.6.0). If you're on Laravel 10/11, pin an older tag or skip it — this is a small package (20 stars, 0 dependents) so you should expect to read the source rather than rely on community answers.
composer require salehhashemi/laravel-repository
php artisan make:repository User
Where to look, in order:
make:repository just generated (app/Repositories/UserRepository.php, UserRepositoryInterface.php, app/Filters/UserFilter.php) — these are the real documentation, more so than the README.vendor/salehhashemi/laravel-repository/src/BaseEloquentRepository.php — the whole method surface (findOne, findAll, findList, paginate, orderBy, addCriteria, lockForUpdate, sharedLock, applyConditions, with) lives there. It's a short read.config/repository.php — publish it now, not later:php artisan vendor:publish --provider="Salehhashemi\Repository\RepositoryServiceProvider" --tag="config"
php artisan config:clear
First use case: bind the interface in a dedicated RepositoryServiceProvider, then inject it into a controller. Keep the first repository dumb — findAll and paginate only — and add filtering/criteria once the binding works and you've confirmed the container resolves it.
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
The triad is the pattern. Every model gets a repository (extends BaseEloquentRepository, implements your interface), an interface (extends RepositoryInterface, plus SearchableRepositoryInterface if you use the trait), and a filter (extends BaseFilter). Both the repository and the filter must implement getModelClass() pointing at the same model — that duplication is the most common source of subtle breakage, so generate them with the command rather than hand-writing.
Filters are for user-driven query params; criteria are for constraints the code owns. PostFilter::applyFilter(array $queryParams) is where request input lands — whereLike, whereValue, compare, dateFrom, dateTo, plus raw getQuery() escapes for relation filtering (whereHas). Criteria (CriteriaInterface::apply(Model, Builder)) are for invariants: FeaturedPostCriteria, a tenant scope, an is_published guard. Don't push user input into criteria — you'll lose the single place where input is validated.
Fluent, then terminal. The repository is a mutable query builder: with(...), orderBy(...), addCriteria(...) all decorate state, then findOne / findAll / paginate / search consume it. The idiomatic shape is a named method on the repository that composes the decoration and ends with the terminal call:
public function searchVisible(array $queryParams, int $perPage): Paginator
{
$this->orderBy('sort');
$this->with('categories');
return $this->search($queryParams, $perPage);
}
This keeps controllers one line and keeps query intent named and testable.
search() is the "filter + paginate" shortcut. With use Searchable and SearchableRepositoryInterface, search($params, $perPage) routes through getFilterManager() → applyFilter($params) → pagination. The upshot: your filter IS your search implementation. Anything you want searchable has to be handled in applyFilter, and a filter that ignores a param silently drops it.
findList('id', 'name') for selects/dropdowns, paginate() for index endpoints, findOne (nullable) vs findOneOrFail (throws ModelNotFoundException) for show endpoints. Add @method docblocks on the interface for the generic return types — the package ships PHPStan level 8, so mirroring that discipline pays off immediately in your own static analysis.
Locking belongs in a transaction. lockForUpdate() / sharedLock() only mean something inside DB::transaction(...); use them in queued jobs and balance mutations, not in web controllers.
Testing tip: don't mock the repository with Mockery. Bind a fake implementation of the interface in the test container, or point the binding at a real repository against SQLite. Mocking the fluent interface is misery because every call returns $this.
PostFilter example has a syntax error. ->compare('amount', '<=' $queryParams['max_amount'] ?? '') is missing a comma. Copy-pasting it will produce a parse error on the first run — fix to '<=', $queryParams['max_amount'] ?? ''.getFilterManager() is abstract-ish. If you use Searchable but don't implement getFilterManager() returning a filter with setQuery($this->getQuery()), search() fails at runtime. The setQuery call is easy to forget and produces confusing "no query" behaviour.addCriteria, orderBy, and with accumulate on the instance. Under Octane, in long-running queue workers, or with $this->app->singleton(...) bindings, criteria and orderings leak between requests/jobs. Use bind (default), and if a repository method adds criteria, be aware a second call on the same instance stacks them.applyConditions/filter clauses append WHERE clauses; applying a criteria before a with() doesn't change joins but does change what findOne matches (it can pivot "first matching" results). Add criteria first, decorate relations last.config/repository.php (env var) and is overridable per call. Passing an invalid page size throws InvalidArgumentException, so validate limit from the request (your controller example passes $request->integer('limit'), which is 0 when absent — make sure your repository treats 0 as "use default" or use a FormRequest with a min:1 rule).applyConditions isn't in the README's "Available Methods" list but is used in the examples. Read the base class before assuming what's available; the README lags the source.Target [XInterface] is not instantiable — and if you're on Laravel 11+, register the provider in bootstrap/providers.php, not config/app.php as the README shows (that snippet is stale for modern skeletons).config:clear step is there for a reason; if you publish config after php artisan config:cache in a deploy, the change won't appear until you re-cache.BaseEloquentRepository for shared behaviour across models, implement CriteriaInterface for reusable constraints, and add your own helpers to a BaseFilter subclass if you find yourself repeating whereHas blocks. The package is intentionally thin — that's the seam to build on.Model::find() adds a layer and an interface for nothing. This package earns its keep when you have repeated, composable query constraints across controllers — filtering, search, criteria, and locking. If a model has one or two simple queries, use Eloquent directly and keep the abstraction budget for where it pays.How can I help you explore Laravel packages today?