nilportugues/repository
nilportugues/repository provides a Laravel repository-pattern implementation that abstracts data access behind clean, testable interfaces. It supports criteria-based queries, caching, and Eloquent integration to keep controllers and services lean.
Install into a Laravel project via Composer and lock the version — this package predates Laravel 5.5 auto-discovery, so expect to register anything manually if it ships a service provider (it generally doesn't; it's framework-agnostic by design).
composer require nilportugues/repository
You first need to understand what you're actually pulling in: it's an interface + abstract base class pair for the Repository pattern in "Domain land" — meaning the package gives you contracts (RepositoryInterface, criteria abstractions) and a couple of concrete/abstract base repositories, but it deliberately does not wire itself into Eloquent, validation, containers, or anything Laravel-specific. That's the whole point and also the first friction you'll hit.
The day-one path is:
RepositoryInterface and the abstract repository class — this is the contract you'll be building against. Everything else follows from the method signatures there.App\Domain\Users\UserRepository, that extends the package contract and adds domain-meaningful methods (findByEmail, activeUsers, etc.).Your first concrete use case is almost always "find a record by a non-primary-key attribute, or a small set of records filtered by domain rules," because that's where the criteria layer earns its keep over raw Eloquent.
Entity-first, model-second. The package's value proposition is that repositories speak in domain entities/objects, not Eloquent models. In practice, a thin Eloquent repository implements your domain interface, and it hydrates/returns your entities. Keep the Eloquent model inside the repository only at the boundary — map to/from your entity so the rest of the app never sees Illuminate\Database\Eloquent\Model.
Criteria for composable queries. The signature feature here is criteria — small, named, testable filter objects. Define one class per domain predicate (ActiveUsers, RegisteredAfter($date), WithRole($role)) and let the repository accept one or compose several. This is how you keep querying logic out of controllers and out of fat repository methods, and it makes each filter unit-testable in isolation.
Interface per aggregate, not per table. Don't make one giant BaseRepository subclass per table. Model repositories around aggregate roots. If the package exposes a generic CRUD base, inherit from it for the boilerplate, but expose only the domain methods via the interface and keep the rest protected/private.
Container binding pattern. In AppServiceProvider (or a dedicated RepositoryServiceProvider):
$this->app->bind(
\App\Domain\Users\UserRepository::class,
\App\Infrastructure\Persistence\EloquentUserRepository::class
);
Use bind (not singleton) unless you're certain the repo is stateless and safe to share — a repository holding query state across requests is a classic bug.
Test seam. Because your interface lives in the domain layer, tests can swap in an in-memory fake repository (backed by arrays) without touching the database. This is the single biggest practical win for day-to-day TDD — write the fake once per interface and your unit tests stop needing RefreshDatabase.
Transactions and side effects. Keep transaction management outside the repository where possible, at the service/application layer. It's tempting to wrap save() in a transaction inside the repo, but once a use case touches two repositories, the boundary belongs above them. If you do put transactions inside the repo, document it loudly.
It's frozen since 2017 — treat it as a pattern, not a dependency. Last release March 2017, PHP 7-era, and 27 stars. There is no active maintenance, no PHP 8 support guarantee, and no security patches. For any project on PHP 8+, either verify it installs cleanly and pin tightly, or (better) copy the interface/criteria abstractions into app/Domain/ and vendor nothing. The pattern is what has value, not the package.
PHP 8 signature collisions. PHP 7-era abstract repository classes frequently clash with PHP 8's stricter magic-method and return-type rules, and ArrayAccess/Iterator implementations often break. Expect warnings or fatal errors around ReturnTypeWillChange. If you hit these, drop the base class and reimplement the small surface yourself — it's usually a few dozen lines.
No framework integration mean you build the persistence adapter. There's no RepositoryServiceProvider, no Eloquent trait, no pagination helper. You write the glue. Budget an afternoon for the first repository and then copy the shape for the rest.
Return your own entities, not Eloquent models. The biggest silent rot in this pattern is a repository that returns User::find(...) — Eloquent models. It compiles, it "works," and it completely defeats the point, because your domain now depends on the ORM. Enforce this with a return type in the interface and, ideally, a PHPStan/Psalm rule.
Criteria composition order matters. If you chain multiple criteria that add where clauses and one of them adds a limit/orderBy/groupBy, ordering is no longer commutative. Document that criteria are applied in order, and keep pagination/ordering criteria separate from filtering criteria.
Pagination is a leaky abstraction here. The package has no pagination contract (it predates where most of the ecosystem settled). Decide early whether pagination stays a presentation concern (repository returns a query builder or a length-aware result set) or whether you add your own PaginatedResult value object. Don't let raw LengthAwarePaginator leak into your domain interface — it will.
N+1 and eager loading. Repositories are the right place to call with([...]) or equivalent, but the criteria pattern makes it easy to forget. Make eager-loading an explicit, named criterion (WithPosts) rather than a side effect of find, so it's visible in use cases and testable.
Debugging: since everything is behind an interface, dd() on a repository call inside the container binding is your fastest check that the right concrete class is resolved. php artisan tinker + app(UserRepository::class)::class tells you what's actually bound in seconds.
Extension points worth using: criteria objects (the best extension point — you can add predicates without touching repositories), and a single generic AbstractRepository that all your concrete repos extend for save/delete/find boilerplate. Resist extending it for anything more clever; every "clever" abstraction in a 2017 DDD package has already cost someone a weekend.
Exit strategy. Pin the version, keep all package-specific code behind your own interfaces, and plan to re-implement the criteria base in ~50 lines if you ever need to drop the dependency — which, given the maintenance status, you probably eventually will.
How can I help you explore Laravel packages today?