drapor/cache-repository
drapor/cache-repository provides a clean repository-style layer over Laravel caching, letting you store, retrieve, and invalidate cached data through a simple, consistent interface. A lightweight package for keeping cache logic organized and easy to swap or extend.
Install and pin
composer require drapor/cache-repository
Before writing any code, open vendor/drapor/cache-repository/composer.json and confirm the Laravel and PHP constraints. This package has very few stars and zero dependents, so it has not been battle-tested across framework versions; pin it to an exact version ("drapor/cache-repository": "1.x" or a commit hash) rather than trusting ^ resolution.
Read the source first
There is no meaningful documentation surface for a package this small. Spend ten minutes on:
ls vendor/drapor/cache-repository/src
cat vendor/drapor/cache-repository/composer.json
You are looking for: the base repository class, whether it is abstract, the cache key strategy (prefix, model class, md5 of query), whether it uses Cache::remember or manual get/put, and whether it implements Illuminate\Contracts\Cache\Repository. Those four facts determine everything else below.
Register the service provider if it is not auto-discovered
Laravel 5.5+ auto-discovers via extra.laravel.providers. If that key is absent, add the provider manually in config/app.php and check whether a config file exists to publish:
php artisan vendor:publish --provider="Drapor\CacheRepository\CacheRepositoryServiceProvider"
First use case: a hot, rarely changing read
The best first target is a query that is read on every request but written rarely — a settings table, a list of categories, a navigation menu. Build one repository, point it at one model, give it a short TTL, and measure. Do not start by wrapping your largest, most write-heavy aggregate; you will spend the day debugging invalidation instead of learning the API.
namespace App\Repositories;
use App\Models\Category;
use Drapor\CacheRepository\...; // confirm the exact class name in src/
class CategoryRepository extends BaseCacheRepository
{
protected function model(): string
{
return Category::class;
}
protected function ttl(): int
{
return 300; // seconds
}
}
Then bind it in a service provider so controllers depend on the abstraction, not the concrete class:
$this->app->bind(CategoryRepository::class, fn () => new CategoryRepository());
One repository per aggregate, not one per model
Treat the repository as the cache boundary for a single aggregate root. If Order and OrderItem are always read together, they belong in one repository with one key namespace. Splitting them creates two caches that can disagree.
Centralize key construction
Every stale-data bug you will hit traces back to a key that was built in two places. Put key generation in one protected method and never hand-write a cache key outside it:
protected function key(string $operation, array $params = []): string
{
return sprintf(
'%s:%s:%s',
config('cache.prefix'),
class_basename($this->model()),
$operation . ':' . md5(serialize($params))
);
}
Including cache.prefix means php artisan cache:clear and per-environment Redis databases behave as expected.
Invalidate on write, inside the same transaction boundary as the write
The safest pattern is model events or an observer, not manual calls scattered across controllers:
protected static function booted(): void
{
static::saved(fn () => Cache::tags(['categories'])->flush());
static::deleted(fn () => Cache::tags(['categories'])->flush());
}
If the package does not support tags, fall back to versioned keys — store an integer version per aggregate in the cache and include it in the key, incrementing it on write. That is the only invalidation strategy that works correctly with the file and database drivers, neither of which supports tags.
Read-through with explicit TTL per method, not per repository
Different methods have wildly different tolerance for staleness. A find($id) can live for an hour; a paginate() filtered by user input should probably not be cached at all. Expose TTL as a parameter defaulted at the method level rather than a single class property:
public function find(int $id, int $ttl = 3600): ?Model
{
return $this->remember($this->key('find', [$id]), $ttl, fn () => $this->model()::find($id));
}
Bind to an interface and fake it in tests
Because a caching repository is a behavior change, not just a performance tweak, bind an interface (CategoryRepositoryInterface) and provide a non-caching implementation for tests. Otherwise your feature tests will pass or fail depending on residual cache state from a previous test.
Bypass for writes and admin screens
Give the repository an escape hatch — a fresh() or withoutCache() variant — and use it in admin panels, exports, and any place where "I just saved it, show me the new value" matters. This one method will save you more debugging time than any other line in the package.
Zero dependents means you are the QA team
With 12 stars and no downstream users, assume the package was written against one Laravel version and never revisited. Check the illuminate/* constraints and test against your actual framework version before rolling it into anything beyond a prototype. If the constraint is loose (^8.0|^9.0), that is not evidence it works — it is evidence nobody checked.
Serialized Eloquent models go stale silently
Caching model instances means relationships, accessors, and appended attributes are frozen at serialization time. If you later change an accessor or add a relationship, cached entries keep returning the old shape until they expire. Cache arrays or DTOs for anything with derived attributes, and always ->fresh() after retrieval if you need live relations.
Tags are driver-dependent
Cache::tags() throws on file and database drivers. If the package's invalidation relies on tags, it will work in your Redis staging environment and fail in production if production uses a different driver. Verify config/cache.php for every environment and prefer the versioned-key approach if you cannot guarantee Redis or Memcached.
Pagination and user-specific filters are cache poison
Any query whose result depends on request input (?page=, ?sort=, ?search=) will generate unbounded cache keys and blow up your Redis memory. Cap it: either do not cache filtered queries, or only cache them for a fixed, small set of enumerated filters. Add a max key count per repository and log when you exceed it.
Cache key collisions across tenants
If you run multi-tenant or per-user scopes, the model class alone is not a unique key. Include the tenant or user ID in every key, ideally via a keyScope() method the repository calls, or you will serve one tenant's data to another — a security bug, not a performance one.
Watch for the N+1 after the cache miss
The first request after expiration hits the database with whatever eager loading the repository applies. If the repository calls Model::all() without with(), you will get a thundering herd of N+1 queries on every cache expiry. Add with() in the repository's base query builder method, and consider a short lock (Cache::lock) around the read-through for expensive queries to prevent cache stampedes.
Debugging: log hits and misses
The package almost certainly does not ship observability. Wrap remember so you can log hit/miss and key name at debug level, then tail the log during a load test. If you cannot tell whether a key is being hit, you cannot tell whether the package is helping at all.
Testing
Use the array cache driver in phpunit.xml and call Cache::flush() in setUp(). Write one test that asserts a second identical read does not touch the database (DB::enableQueryLog() plus a query count assertion). That test is the entire value proposition of the package in one assertion.
Extension points to look for
Before you fork, check whether the base class exposes protected key(), ttl(), query(), and remember() methods. If it does, subclass and override — that is a clean seam. If everything is private or the class is final, do not fight it; copy the class into app/Support/ and own it. For a package this size, owning ~150 lines is often cheaper than tracking upstream changes that will never come.
How can I help you explore Laravel packages today?