symfony/cache-contracts
A set of cache abstractions extracted from Symfony components. It defines interfaces such as CacheInterface, CacheItemInterface, and TagAwareCacheInterface, letting libraries depend on proven cache semantics without binding to a concrete implementation.
You almost certainly already have it. symfony/cache-contracts is a transitive dependency pulled in by symfony/cache, which Laravel has shipped with since 5.5. Verify with composer why symfony/cache-contracts — you won't normally add it explicitly.
Where to look first:
vendor/symfony/cache-contracts/:
CacheInterface + CacheTrait (the "simple" PSR-16-ish API: get, set, delete, getMultiple, setMultiple, deleteMultiple, has, clear)CacheItemPoolInterface (PSR-6 alias re-exported from psr/cache)CacheItemInterface (PSR-6)TagAwareCacheInterface + ItemInterface (Symfony extension: tag invalidation)CallbackInterface (supports the CallbackInterface-based deferred save pattern)Illuminate\Cache\ArrayStore, Illuminate\Cache\RedisStore, and Illuminate\Cache\Repository to see how the framework actually consumes these concepts.First use case for a Laravel dev: don't implement anything. Just confirm the contracts are available and reach for Cache facade as normal — Cache::remember(), Cache::tags([...])->flush(), etc. are already implemented on top of these abstractions. The contracts only become interesting when:
Cache facade.Psr\Cache\CacheItemPoolInterface (PSR-6) directly.Inject the contract, not the driver. In a service or package, type-hint Symfony\Contracts\Cache\CacheInterface (simple API) or Psr\Cache\CacheItemPoolInterface (PSR-6) rather than Illuminate\Contracts\Cache\Repository. In a Laravel service provider, bind a specific adapter:
$this->app->bind(CacheInterface::class, function ($app) {
return new Psr16Cache(new ArrayAdapter()); // or FilesystemAdapter, RedisAdapter, ...
});
You'll typically pull the underlying adapter from symfony/cache — the contracts package has zero concrete implementations.
Custom Laravel driver (the classic use case). Implement Laravel's Illuminate\Contracts\Cache\Store and wire it into config/cache.php:
// config/cache.php
'dynamo' => ['driver' => 'dynamo', ...],
// AppServiceProvider::boot()
Cache::extend('dynamo', function ($app) {
return Cache::repository(new DynamoStore(...));
});
If your driver needs to also satisfy third-party packages, additionally implement Psr\Cache\CacheItemPoolInterface on top of it (usually via Symfony\Component\Cache\Adapter\... or a thin wrapper).
Deferred/batched writes. Symfony's adapters support saveDeferred() + commit() for bulk writes (relevant when porting Symfony code into Laravel). In native Laravel, you'd lean on Cache::putMany() instead — the contracts don't add much here.
Tag invalidation. Laravel's Cache::tags([...])->flush() uses the same conceptual interface as Symfony's TagAwareCacheInterface, but they are different interfaces. Don't assume you can inject TagAwareCacheInterface and have Laravel pick it up — Laravel's TaggedCache is its own thing. Keep tags inside the Laravel facade; use TagAwareCacheInterface only if you're writing Symfony-compatible code.
Package authoring. If you publish a Laravel package that caches, type-hint CacheInterface and ship a Psr16Cache(ArrayAdapter::class) fallback so it works with or without Laravel's cache layer. This is the single biggest win the package gives you — a portable, tiny surface area to depend on.
It's interfaces only. symfony/cache-contracts won't cache anything by itself. You need symfony/cache (or another PSR-6/PSR-16 implementation) behind it. In Laravel this is a non-issue because the framework already includes it — but if you're writing a package, add symfony/cache to your require (or suggest).
PSR-6 vs PSR-16 vs Symfony "simple". Three different things are conflated in people's heads:
Psr\Cache\CacheItemPoolInterface — PSR-6, item-based (getItem, save).Psr\SimpleCache\CacheInterface — PSR-16, key/value (get, set).Symfony\Contracts\Cache\CacheInterface — Symfony's simple-cache-like interface, not strictly PSR-16. It's a subset with cache.get-style callbacks in the concrete adapters.If you need PSR-16 specifically, reach for Psr\SimpleCache\CacheInterface and bridge via Symfony\Component\Cache\Psr16Cache.
Version skew. symfony/cache-contracts v2/v3 aligns with Symfony 5/6+ and PSR-6/PSR-16 versions. Laravel pins a compatible range; if you composer require an incompatible version, composer will complain. Never force it. Use composer why symfony/cache-contracts to confirm the version pulled in by Laravel before adding an explicit constraint.
ArrayAdapter is memory-only. Great for tests (bind it in TestCase::setUp() to keep tests hermetic), useless in production since it doesn't persist across requests. This is the classic "why is my cache empty?" bug when a Symfony snippet is dropped into Laravel.
Serialization quirks vary by adapter. FilesystemAdapter and RedisAdapter serialize non-scalar values; some setups don't. If you're storing value objects, be explicit about serialization rather than relying on adapter defaults.
Debugging stale cache. php artisan cache:clear is the Laravel-side tool. There's no equivalent cache:clear from the contracts package — it just declares clear(). If a third-party package implements CacheInterface but no-ops clear(), you'll chase ghosts.
Extension points for custom adapters. If you must build from symfony/cache, extend AbstractAdapter and implement doFetch, doSave, doDelete, doClear. For a pure-Laravel driver, implement Illuminate\Contracts\Cache\Store instead — the contracts sit underneath, not beside, Laravel's abstraction, and mixing the two usually leads to bugs.
Don't depend on it directly unless you must. For application code, Illuminate\Contracts\Cache\Repository is the right abstraction. Reserve symfony/cache-contracts for interop boundaries (packages, Symfony-side libraries, PSR-6 consumers) so you don't accidentally couple business code to Symfony.
How can I help you explore Laravel packages today?