matthewbdaly/laravel-repositories
Treat this package as a reference implementation, not a framework dependency. It is a handful of classes — a base Eloquent repository, an interface, and a caching decorator — last tagged in mid-2018. The value is the pattern and the ~150 lines of code you can read in ten minutes, not the composer require.
Minimal setup
composer require matthewbdaly/laravel-repositoriesvendor/matthewbdaly/laravel-repositories/composer.json. On Laravel 9/10/11 the illuminate/* constraint will almost certainly block installation. If it does, either pin the package in a legacy service or copy src/ into app/Support/Repositories/ and own it. Given the size, copying is usually the right call.src/ before writing anything. There are only a few files: the interface, the base repository, and the cache decorator. Confirm how the base class resolves the model (a protected $model property vs. a model() method — this differs between the author's packages and blog posts) and which methods the interface actually declares.First repository
namespace App\Repositories;
use App\Models\User;
use Matthewbdaly\LaravelRepositories\Repositories\BaseRepository;
class EloquentUserRepository extends BaseRepository
{
protected $model = User::class; // or: public function model() { return User::class; }
}
Bind it once, in a dedicated RepositoryServiceProvider:
$this->app->bind(
\App\Repositories\UserRepositoryInterface::class,
\App\Repositories\EloquentUserRepository::class
);
First use case
Start with a single read-heavy controller — a listing page or an API endpoint — and inject the interface, never the Eloquent class. The payoff only appears when you swap the concrete binding (cache decorator on/off, different persistence) without touching the consumer. If you can't name the second implementation you'd ever bind, skip the abstraction and use Eloquent directly.
Interface-first, base-class-second. Define your own UserRepositoryInterface extending the package interface, so the package's method surface is an implementation detail you can replace later. The package interface is a floor, not a contract you should leak into controllers.
Constructor injection everywhere. Bind the interface in a service provider and type-hint it in controllers, jobs, and console commands. The base repository is a plain class — it resolves cleanly from the container with no facade magic, which makes it trivially mockable:
$this->mock(UserRepositoryInterface::class, function ($mock) {
$mock->shouldReceive('find')->with(1)->andReturn(new User(['id' => 1]));
});
Caching decorator — wrap, don't inherit. The decorator pattern here is the genuinely useful bit. Compose it around the real repository rather than adding cache logic to the base class:
$this->app->bind(UserRepositoryInterface::class, function ($app) {
return $app->make(CacheableUserRepository::class);
});
Where the cacheable implementation holds an inner UserRepositoryInterface and delegates. Verify the decorator's constructor signature against source — you may need to pass the inner repository and a cache store/TTL explicitly.
Invalidate on write, not on read. Reads go through Cache::remember; create/update/delete must flush the affected keys (or the whole tag/store). If the shipped decorator doesn't do this, that is the first thing you extend — a stale-cache bug is the single most likely production incident with this package.
Eager loading via a $with property. Most base repositories of this vintage support a protected $with = ['posts'] applied to every query. Use it for the two or three relations every caller needs; pass per-call with() for the rest. Avoid making it a dumping ground — it silently affects every query in the app.
Service-layer wrapper for multi-step writes. The base repository is CRUD-shaped. Anything transactional (create order + line items + decrement stock) belongs in an action/service class that calls the repository, not in a createWithItems() method bolted onto the repository. Keeping the repository thin is what makes the interface swappable.
Testing. Bind the array cache driver in phpunit.xml so the decorator doesn't leak state between tests, and assert on the repository interface in unit tests rather than hitting the database through the base class. Use RefreshDatabase for the Eloquent-level tests only.
The package is effectively abandoned. Twelve stars, last release June 2018, no Laravel 6+ activity. Budget for it breaking on PHP 8 (illuminate/support constraint) and for security scanners flagging an unmaintained dependency. The correct long-term move is to fork the two or three classes into your own namespace. Do that on day one unless you have a hard reason not to.
It is not persistence-agnostic. The base class is Eloquent to the core — it returns Collections, Model instances, and LengthAwarePaginators. The interface buys you testability and cache-swapping, not a future move to an API or document store. Don't sell it internally as a portability layer.
Model resolution quirks. If model resolution goes through app($this->model) rather than new $this->model, the container will inject constructor dependencies into your models — occasionally useful, frequently surprising. Check BaseRepository's constructor and any newQuery() helper before relying on either behaviour.
Cache key collisions. If the decorator derives keys from method name plus arguments, two repositories sharing a key namespace will collide (find(1) for users vs. posts). Namespace keys by repository class. If the driver supports tags, tag by model; if it's file or database, tags silently don't work in older Laravel — verify your driver.
Debugging stale reads. First suspect the decorator, not Eloquent. Confirm the binding order in the provider (the decorator must wrap the inner repo, not itself), then Cache::flush() locally and re-run. Log the generated key and TTL once — it's the fastest way to spot a missing invalidation.
Bypass the cache deliberately. Provide an escape hatch (a fresh() or direct Eloquent call) for admin screens and post-write redirects, otherwise users will see their own edits disappear for the TTL window.
Extension points worth knowing. Override the model property/method, the base query builder hook (newQuery()), the eager-load $with array, and — in the decorator — the key generator and TTL. Those four cover ~95% of real customization. Anything beyond that, write your own method rather than fighting the base class; it has no documented extension contract.
Don't decorate everything. Caching a repository whose reads are already cheap (single indexed find()) adds a cache round-trip and an invalidation bug surface for no gain. Measure with a query log first; decorate the listing/search paths where the win is real.
How can I help you explore Laravel packages today?