Skip to content

Doctrine Second-Level Cache

SLC caches entity/association state across EntityManagers. The core constructs Doctrine's DefaultCacheFactory using a supplied PSR-6 pool. The CI4 adapter constructs that pool from the framework cache configuration.

Standalone setup

Configure SLC before constructing Doctrine and inject a persistent pool:

use Daycry\Doctrine\Cache\CachePools;
use Daycry\Doctrine\Doctrine;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$config->secondLevelCache = true;
$config->secondLevelCacheStatistics = true;
$config->secondLevelCacheTtl = 3600;
$config->secondLevelCacheLockPath = __DIR__ . '/var/locks/doctrine';
$pools = new CachePools(
    secondLevel: new FilesystemAdapter('primary_slc', 0, __DIR__ . '/var/cache'),
);
$doctrine = new Doctrine($config, $pools);

Here $config is the application's core Configuration, with connection and entity paths already set. Query/result/metadata pools still use their ArrayAdapter defaults unless also supplied. Without an injected persistent SLC pool, SLC is process-local.

CodeIgniter setup

In the published app/Config/Doctrine.php:

public bool $secondLevelCache = true;
public bool $secondLevelCacheStatistics = true;
public ?int $secondLevelCacheTtl = null;

The adapter reuses the Config\Cache backend (file, Redis, Memcached or array). Its namespace is the framework prefix plus doctrine_slc and, for a non-default group, the hex-encoded group suffix. Each group has its own manager and pools.

Lifetimes

secondLevelCacheTtl Standalone CI4 adapter
null Region lifetime 0 Inherit Config\Cache::$ttl
0 No expiry until invalidation No expiry until invalidation
Positive integer Region lifetime in seconds Override the framework TTL for SLC

Configure the injected standalone pool's own lifetime consistently with the desired region lifetime. A backend may evict entries earlier according to its storage policy.

Mark entities as cacheable

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Cache(usage: 'READ_ONLY')]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 120)]
    private string $name;
}
Usage Suitable data
READ_ONLY Immutable reference data
NONSTRICT_READ_WRITE Mutable data where eventual cache consistency is acceptable
READ_WRITE Mutable data requiring concurrent region locking

Associations also need suitable cache mappings. Changing mappings requires clearing stale metadata and invalidating the corresponding data regions.

READ_WRITE locking

The core requires an explicit secondLevelCacheLockPath for READ_WRITE regions. The CI4 adapter defaults to a group-specific directory under WRITEPATH . 'cache/doctrine/locks'.

Processes sharing regions must share this lock directory. Give it write access for the application process. File locks on independent hosts do not coordinate unless the directory is shared; account for this when choosing deployment storage.

Invalidation and statistics

ORM writes invalidate affected cache state according to the configured strategy. Writes performed outside ORM need an application invalidation strategy. Manual invalidation is also needed when changing mappings or deploying incompatible cached entity layouts. Spark doctrine:cache:clear clears only query/result/metadata pools and does not evict SLC regions.

$cache = $doctrine->getEm()->getCache();
$cache?->evictEntityRegion(Product::class);

Statistics are available through $doctrine->getSecondLevelCacheLogger() when enabled. See SLC statistics. The CI4 toolbar can display them; the core does not depend on a toolbar.

Troubleshooting

  • Use a persistent pool when testing cache reuse across processes.
  • Ensure entity mappings contain #[ORM\Cache(...)].
  • Ensure file cache and lock directories are writable.
  • For native Redis/Memcached pools, load the PHP extension and verify the service connection; having the service running alone does not load the extension.
  • Doctor checks general cache access and SLC configuration, not SLC region contents.