Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Adapter Laravel Package

cache/doctrine-adapter

This package adapts legacy Doctrine\Common\Cache implementations to PSR-6 and PSR-16, letting existing Doctrine caches work with modern cache pools. Version 3 changes the tagged-item format, so drain workers and clear the cache before upgrading or rolling back.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package actually is: a ~80-line adapter that lets a Doctrine\Common\Cache\Cache instance (Memcached, Redis, APCu, Filesystem, Array) be consumed through Psr\Cache\CacheItemPoolInterface. It is not a Laravel cache driver, not a taggable pool, and it does not replace Illuminate\Cache. Treat it as a boundary object between a legacy Doctrine-configured cache and PSR-6 consumers.

Step 1 — Install with the Doctrine version pinned to 1.x.

composer require cache/doctrine-adapter doctrine/cache:^1.13

Doctrine\Common\Cache\* drivers were removed in doctrine/cache 2.0. If anything in your tree pulls doctrine/cache:^2, the adapter will not install and the driver classes won't exist. Check first:

composer why doctrine/cache
php -r "var_dump(class_exists(Doctrine\Common\Cache\RedisCache::class));"

Step 2 — Look at two files, in this order:

  1. vendor/cache/doctrine-adapter/src/DoctrineCachePool.php — you can read it end-to-end in a minute. Everything surprising about this package lives there.
  2. vendor/cache/adapter-common/src/CachePool.php + CacheItem.php — the real implementation (PSR-6 mechanics, key validation, expiry mapping). The Doctrine adapter is a thin save/fetch/contains/delete/clear shim over it.

Step 3 — First use case, in php artisan tinker:

use Cache\Adapter\Doctrine\DoctrineCachePool;
use Doctrine\Common\Cache\ArrayCache;   // swap for RedisCache/ApcuCache later

$pool = new DoctrineCachePool(new ArrayCache());

$item = $pool->getItem('reports.daily.2024-06-01');
if (! $item->isHit()) {
    $item->set(['rows' => 128])->expiresAfter(300);
    $pool->save($item);
}
dump($item->isHit(), $item->get());

If that round-trips, the only remaining work is deciding where the $pool comes from in your app.

Step 4 — Decide your integration shape. In Laravel you have two options, and they are very different:

  • Use the pool directly in a service/repository class (recommended). Bind it as a singleton, inject it, call getItem()/save(). No Laravel cache semantics, no events, no Cache:: facade.
  • Wrap it in an Illuminate\Contracts\Cache\Store so Cache::store('doctrine')->remember(...) works. More code, but you keep Laravel ergonomics (see below).
// AppServiceProvider::register()
$this->app->singleton(DoctrineCachePool::class, function () {
    $redis = new \Redis();
    $redis->connect(config('database.redis.default.host'), 6379);

    $doctrine = new \Doctrine\Common\Cache\RedisCache();
    $doctrine->setRedis($redis);
    $doctrine->setNamespace('acme:psr6');   // keep it away from ORM query cache

    return new DoctrineCachePool($doctrine);
});

Implementation Patterns

Pattern 1 — Reuse an already-configured Doctrine cache (the main reason to be here)

The value proposition is sharing infrastructure that a legacy/Doctrine bundle already created: same Memcached pool, same Redis connection, same namespace conventions, same ops runbook. Wrap it and hand it to PSR-6 consumers.

$memcached = new \Memcached();
$memcached->addServer('localhost', 11211);

$doctrineCache = new \Doctrine\Common\Cache\MemcachedCache();
$doctrineCache->setMemcached($memcached);
$doctrineCache->setNamespace('reports');

$pool = new DoctrineCachePool($doctrineCache);

setMemcached() / setRedis() are mandatory — the driver lazily uses whatever you injected, and forgetting them produces a fatal on first fetch() rather than a sane exception.

Pattern 2 — Expose the pool as a Laravel cache store

If you want Cache::store('doctrine'), do not add a 'driver' => 'doctrine' entry to config/cache.php — Laravel has no such driver and will throw InvalidArgumentException: Driver [doctrine] is not supported. Register it with Cache::extend() in AppServiceProvider::boot():

use Illuminate\Cache\Repository;
use Illuminate\Contracts\Cache\Store;
use Psr\Cache\CacheItemPoolInterface;

class DoctrinePoolStore implements Store
{
    public function __construct(
        private CacheItemPoolInterface $pool,
        private string $prefix = ''
    ) {}

    public function get($key)
    {
        $item = $this->pool->getItem($this->prefix.$key);

        return $item->isHit() ? $item->get() : null;
    }

    public function many(array $keys)
    {
        $items = $this->pool->getItems(
            array_map(fn ($k) => $this->prefix.$k, $keys)
        );

        $values = [];
        foreach ($keys as $key) {
            $item = $items[$this->prefix.$key];
            $values[$key] = $item->isHit() ? $item->get() : null;
        }

        return $values;
    }

    public function put($key, $value, $seconds)
    {
        $item = $this->pool->getItem($this->prefix.$key)
            ->set($value)
            ->expiresAfter($seconds ?: null);   // 0 => forever in Laravel, null in PSR-6

        return $this->pool->save($item);
    }

    public function putMany(array $values, $seconds) { /* loop put() */ }

    public function increment($key, $value = 1) { /* get() + put(), not atomic */ }

    public function decrement($key, $value = 1) { /* get() - put(), not atomic */ }

    public function forever($key, $value) { return $this->put($key, $value, null); }

    public function forget($key) { return $this->pool->deleteItem($this->prefix.$key); }

    public function flush() { return $this->pool->clear(); }

    public function getPrefix() { return $this->prefix; }
}

// boot()
Cache::extend('doctrine', function ($app) {
    $store = new DoctrinePoolStore(
        $app->make(DoctrineCachePool::class),
        $app['config']['cache.prefix'].':'
    );

    return Cache::repository($store);
});

Then config/cache.php: 'doctrine' => ['driver' => 'doctrine'] works, and Cache::store('doctrine')->remember(...) behaves normally. Note that recent Laravel versions add touch() to the Store contract — implement the full interface and you won't break on upgrade.

Pattern 3 — Keep it off the default store

Do not set CACHE_DRIVER=doctrine globally. Use it explicitly for one concern (report aggregates, third-party API payloads, expensive taxonomy trees) and leave sessions, queues, rate limiters, and Cache::lock() on Redis/database. This adapter has no atomic increments and no locks.

Pattern 4 — ArrayCache as a test double

$this->app->instance(
    DoctrineCachePool::class,
    new DoctrineCachePool(new \Doctrine\Common\Cache\ArrayCache())
);

Clean, per-process, no mocking required. It behaves like Laravel's array store — including not sharing state across PHP processes, so integration tests that shell out won't see it.

Pattern 5 — Tags, if you really need them

DoctrineCachePool is not taggable, and Doctrine\Common\Cache\MemcachedCache has no tagging either. The php-cache ecosystem's answer is a decorator:

use Cache\Taggable\TaggablePSR6PoolAdapter;

$taggable = TaggablePSR6PoolAdapter::makeTaggable(
    new DoctrineCachePool($doctrineCache)
);

Cache\Taggable implements tags by writing a tag-index item per tag and invalidating through it — read the source before trusting it in production, because invalidation is O(tags) reads/writes and there is no atomicity guarantee across the index and the entries. If tag invalidation is a hard requirement, Laravel's Redis store (TaggableStore) is the safer path.

Pattern 6 — Pre-warming from the console

PhpFileCache-style drivers let you generate a cache during deploy and read it at runtime:

// php artisan cache:warm
$pool = new DoctrineCachePool(new \Doctrine\Common\Cache\FileCache(storage_path('framework/doctrine')));
$pool->save($pool->getItem('taxonomy.tree')->set($tree)->expiresAfter(null));

Only do this with a writable, persistent path — see gotchas below.


Gotchas and Tips

Laravel's Cache::tags() will explode on this pool. TagSet builds keys containing :, and PSR-6 forbids { } ( ) / \ @ : in keys. DoctrineCachePool::getItem() throws InvalidArgumentException (from Cache\Adapter\Common\CacheKey::validateKey()). Same for any Eloquent/URL-derived key like user:1 or api/v2/products. Sanitize before it reaches the pool:

$key = strtr($raw, ['{' => '.', '}' => '.', '(' => '.', ')' => '.',
                    '/' => '.', '\\' => '.', '@' => '.', ':' => '.']);

Better: hash the key (sha1($raw)) — it sidesteps PSR-6 restrictions, the Memcached 250-byte limit, and the stricter charset that the underlying driver may silently enforce (MemcachedCache is far pickier than PSR-6; it doesn't error, it mangles).

The pool stores CacheItem objects in the backend, not raw values. That has three consequences:

  • The driver must serialize objects. FileCache, RedisCache, MemcachedCache, ArrayCache are fine. PhpFileCache is not — it uses var_export(), which requires __set_state() on CacheItem and will produce broken output. Avoid it.
  • Any change to cache/adapter-common or your own value classes can leave unserializable entries behind. Flush the namespace on deploy, or version the key prefix (reports:v2:).
  • Inspecting Redis/Memcached with redis-cli will show a serialized PHP object. Don't expect human-readable values, and don't let another service read the same namespace.

flush() is a namespace-wide napalm. If your pool shares a namespace with Doctrine ORM's query/metadata cache, php artisan cache:clear (routed through DoctrinePoolStore::flush()) nukes ORM caches too. Always setNamespace('acme:psr6') on the Doctrine provider and keep it distinct.

No observability for free. Bypassing Illuminate\Cache\Repository means no CacheHit/CacheMissed/KeyWritten events, so Telescope and Debugbar show nothing, and cache:clear/cache:forget only work if you built the Store wrapper. If you use the raw pool, add your own instrumentation or you will be debugging blind.

Increment/decrement are read-modify-write. PSR-6 has no atomic counters. Rate limiters, view counters, and Cache::increment() semantics will race under load. Keep those on Redis.

Driver-specific landmines:

  • RedisCache needs phpredis (\Redis), not Predis — use PredisCache for Predis.
  • ApcuCache only shares state within a single PHP-FPM pool on a single host; multi-pod deployments get N independent caches.
  • FileCache/FileCache::setDirectory() — the default is sys_get_temp_dir(), which in containers is pod-ephemeral, so caches evaporate on restart and differ between replicas.
  • Memcached: 250-byte key limit and a 1 MB value limit; both fail quietly at the driver level.

Expiry mapping is relative seconds, and Doctrine's 0 means forever while PSR-6's null means forever. The adapter handles the mapping, but your DoctrinePoolStore::put() must translate Laravel's $seconds === 0 to expiresAfter(null) or you'll write items that instantly expire.

Deferred saves need a commit(). saveDeferred() queues into the pool and does nothing until commit(). There's no Laravel hook that calls it — if you use deferred writes, commit explicitly at end of request or don't use them.

Maintenance reality check. This is a [READ-ONLY] split repo (issues/PRs live in php-cache/cache), 13 stars, last release years old, and its sole job is to bridge a deprecating API (doctrine/cache 1.x, removed in ORM 3). The whole implementation is small enough to vendor into app/Support/DoctrineCachePool.php in an afternoon, which removes the abandoned-dependency risk entirely. Do that if this is going anywhere long-lived.

Alternatives worth pricing out before committing:

  • symfony/cache ships Symfony\Component\Cache\Adapter\DoctrineAdapter, a maintained and better-tested equivalent — usually the better choice if you already have Symfony components via Laravel's internals.
  • The reverse bridge, cache/doctrine-bridge (PSR-6 → Doctrine), is what you want if the goal is to feed Laravel/Redis caches into Doctrine ORM's query cache — a much more common Laravel need than this direction.
  • If the only goal is "PSR-6 pool in Laravel," cache/redis-adapter / cache/filesystem-adapter skip Doctrine entirely.

Testing/forking tip: the php-cache org publishes cache/integration-tests with a CachePoolTest trait that runs the full PSR-6 compliance suite. Use it if you fork or build your own Store wrapper — it catches the key-validation and expiry edge cases you'd otherwise ship.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager