Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/Cache.md for this page in Markdown format</div>
Cache System
Overview
The cache system is a three-tier architecture with PSR-6 compliance, backed by pluggable adapters and a legacy wrapper for CodeIgniter integration.
Application Code
|
v
+-----------+ Legacy CI wrapper (model/library caching)
| Pscache |
+-----------+
|
v
+-----------+ PSR-6 CacheItemPoolInterface
| CachePool |
+-----------+
|
v
+----------------+ CacheAdapterInterface
| CacheAdapter |
+----------------+
/ | \
v v v
+------+ +-------+ +------+
| File | | Redis | | APCu | ... Concrete adapters
+------+ +-------+ +------+Cache Tiers
| Tier | Alias | Default Adapter | Lifetime | Purpose |
|---|---|---|---|---|
| L0 | cache.l0 | InMemoryAdapter | Single request | Dedup within one HTTP request |
| L1 | cache.l1 | ChainAdapter (auto) | Server-local, cross-request | Hot data (categories, settings) |
| L2 | cache.l2 | FileAdapter | Persistent (hours–days) | DB query results, model output |
L2 is the primary cache tier. L1 is an optional fast layer. L0 is a simple in-process store.
L2 — Persistent Cache
The default and most-used tier. All Pscache calls go through CachePool, which uses the L2 adapter.
Adapters: FileAdapter (default), RedisAdapter, RedisClusterAdapter.
L1 — Server-Local Cache
Optional fast layer for data that must survive across requests but doesn't need to be shared across servers. Configured via APP_CACHE_L1_ADAPTER:
auto(default): UsesChainAdapterto try APCu first, then SHM, falling back to L2 if neither is healthy.none: Disabled;cache.l1becomes an alias forcache.l2.- FQCN: Use a specific adapter class directly (e.g.
Advisable\Cache\Adapter\ApcuAdapter).
L0 — In-Memory Cache
Always InMemoryAdapter. Lives only for the current request. No configuration needed.
Accessing Tiers
php
// L2 — via PSR-6 pool (most common)
$pool = di()->get(\Advisable\Cache\CachePool::class);
// Direct adapter access by tier
$l2 = di()->get('cache.l2'); // CacheAdapterInterface
$l1 = di()->get('cache.l1'); // CacheAdapterInterface (ChainAdapter in auto mode)
$l0 = di()->get('cache.l0'); // InMemoryAdapterAdapters
All adapters implement CacheAdapterInterface (src/Cache/Adapter/CacheAdapterInterface.php):
php
interface CacheAdapterInterface
{
public function fetch(string $key): mixed;
public function store(string $key, mixed $data, int $ttl = 0): bool;
public function delete(string $key): bool;
public function deleteByPattern(string $pattern): int;
public function clear(): bool;
public function clearNamespace(string $namespace): bool;
public function storeIfAbsent(string $key, mixed $data, int $ttl = 0): bool;
public function exists(string $key): bool;
public function getTimestamp(string $key): ?int;
public function isHealthy(): bool;
}FileAdapter
- Storage: Filesystem (default
../cache/) - Key mapping: Dotted keys become directory paths; filenames are SHA-256 hashed.
user.profile.abcbecomescache/user/profile/<sha256>.cache. - Serialization: PHP
serialize() - Constructor:
(string $connectionString = '', string $keyPrefix = '', string $tenantPrefix = '', array $config = []) - Prefix behavior: Resolved prefix becomes a subdirectory under the base path.
- Delete script: Supports an OS-level delete script (
cache/delete.shon Linux,cache/delete.baton Windows) for fast bulk clearing. The Linux script uses a rename-then-delete strategy to stay safe under two failure scenarios:- Disk-full condition: A naive approach that pre-creates the replacement directory before renaming requires disk space before any space is freed — failing at precisely the moment you need the clear most. The script instead renames the live language directory (e.g.
el/) todel/first (atomic, zero disk space cost), then creates a fresh empty replacement directory (only ~4 KB), then deletesdel/. Because the rename frees the reference to all old cache inodes before any allocation is needed, the clear succeeds even on a completely full disk. - Interrupted-run leftover: If a previous run was interrupted after the rename but before
rm -rf del/, a staledel/directory remains on disk. Without cleanup, a subsequentmv el delwould moveel/inside the existingdel/subtree instead of replacing it, causing unpredictable cache state. The script removes any leftoverdel/at the very start of each run withrm -rf del 2>/dev/nullto prevent this.
- Disk-full condition: A naive approach that pre-creates the replacement directory before renaming requires disk space before any space is freed — failing at precisely the moment you need the clear most. The script instead renames the live language directory (e.g.
- TTL: Not enforced by the adapter — handled by
CachePool's envelope expiry check.
RedisAdapter
- Storage: Single Redis instance
- Key mapping: Dotted keys become colon-separated.
user.profile.abcbecomesprefix:cache:l2:user:profile:abc. - Serialization: Configurable (PHP/JSON/Igbinary/Msgpack) with optional compression (LZ4/ZStd).
- Constructor:
(string $connectionString, string $keyPrefix = '', string $tenantPrefix = '') - Connection string:
tcp://host:6379?prefix=tenant&database=0or Unix socket. - TTL: Native Redis TTL via
SETEX.
RedisClusterAdapter
Like RedisAdapter but for Redis Cluster. Connection string accepts multiple comma-separated hosts: tcp://host1:6379,tcp://host2:6379?prefix=tenant
ApcuAdapter
- Storage: PHP APCu shared memory (process-level, survives requests under PHP-FPM)
- Key format:
tenant:cache:l1:originalkey - Constructor:
(string $connectionString = '', string $keyPrefix = 'l1', string $tenantPrefix = '') - Health:
isHealthy()checksapcu_enabled().
ShmAdapter
- Storage:
/dev/shmfilesystem (Linux shared memory) - Key mapping: MD5-hashed filenames as
.shmfiles. - Serialization: JSON with TTL expiry envelope (
{'k','v','e'}). - Constructor:
(string $connectionString = '', string $keyPrefix = '', string $tenantPrefix = '') - Prefix requirement: Requires a tenant prefix for isolation. Without one,
isHealthy()returns false (directory is empty string). - Directory creation: Deferred to first
store()/storeIfAbsent()call.
InMemoryAdapter
- Storage: PHP array, cleared at end of request.
- No configuration. Always healthy.
ChainAdapter
- Purpose: Health-aware adapter selection. Iterates an ordered list of candidate adapters and delegates to the first one where
isHealthy()returns true. - Constructor:
(array $candidates, ?CacheAdapterInterface $fallback = null) - No adapter-specific knowledge. Selection is purely driven by each adapter's
isHealthy(). - Default L1 wiring:
[ApcuAdapter, ShmAdapter]with L2 as fallback.
Connection Strings
All configurable adapters accept a $connectionString parameter. The ?prefix= query parameter provides tenant isolation.
FileAdapter: /path/to/cache/?prefix=tenant
ShmAdapter: /dev/shm/ecommercen?prefix=tenant
ApcuAdapter: ?prefix=tenant
RedisAdapter: tcp://host:6379?prefix=tenant&database=0
RedisClusterAdapter: tcp://host1:6379,tcp://host2:6379?prefix=tenantPrefix precedence: ?prefix= in connection string > _PREFIX env var > empty.
CachePool (PSR-6)
Advisable\Cache\CachePool implements Psr\Cache\CacheItemPoolInterface and GroupableCacheInterface.
Wraps the L2 adapter with a data envelope:
php
// Internal storage format
[
'd' => $data, // The actual cached value
'c' => time(), // Created timestamp
'e' => $expiry, // Expiry timestamp (null = no expiry)
]Key Methods
php
$pool = di()->get(CachePool::class);
// Standard PSR-6
$item = $pool->getItem('my.cache.key');
if (!$item->isHit()) {
$item->set($expensiveResult);
$item->expiresAfter(3600);
$pool->save($item);
}
$data = $item->get();
// Deferred saves (buffered, flushed with commit)
$pool->saveDeferred($item);
$pool->commit();
// Deletion
$pool->deleteItem('my.cache.key');
$pool->deleteItems(['key1', 'key2']);
// Group operations (GroupableCacheInterface)
$pool->deleteByPattern('en.product_model.*');
$pool->deleteAll('reports/monthly'); // Clear namespace
$pool->clear(); // Clear everythingKey Validation
PSR-6 compliant. Keys must be strings. The following characters are forbidden: {}()/\@:
CacheItem (PSR-6)
Advisable\Cache\CacheItem implements Psr\Cache\CacheItemInterface.
php
$item = $pool->getItem('key');
$item->getKey(); // 'key'
$item->isHit(); // Was it found?
$item->get(); // Value or null
$item->set($value); // Set value (chainable)
$item->expiresAt(new DateTime('+1 hour'));
$item->expiresAfter(3600); // TTL in seconds
$item->expiresAfter(new DateInterval('PT1H'));Pscache (Legacy Wrapper)
application/libraries/Pscache.php — the CodeIgniter-facing cache interface. Loaded as $this->pscache in controllers.
Wraps CachePool with model/library method caching, dependency tracking, and language-aware key generation.
Method Caching
The primary usage pattern. Calls a model or library method and caches the result:
php
// Cache a model method call
$data = $this->pscache->model('product_model', 'get_all', [$categoryId]);
// Cache a library method call with custom TTL
$zoneId = $this->pscache->library('cloudflare', 'getZoneId', [$domain], 3000);Internally, Pscache generates a cache key as {lang}.{property}.{sha1(method+args)}, calls the method if not cached, and stores the result.
Cache Key Format
{language_abbr}.{model_or_library_name}.{sha1(method + serialized_args)}Example: en.product_model.a1b2c3d4e5f6...
Pscache Data Envelope
Pscache wraps data with its own metadata inside the CachePool envelope:
php
[
'__cache_trace_params' => ['property' => ..., 'method' => ..., 'arguments' => ...],
'__cache_contents' => $actualData,
'__cache_created' => time(),
'__cache_expires' => time() + $ttl,
'__cache_dependencies' => ['dep_key_1', 'dep_key_2'],
]Dependency Tracking
A cached item can declare dependencies on other cache keys. On retrieval, Pscache checks whether any dependency was modified after the item was created. If so, the item is treated as stale and deleted.
Group Deletion
php
// Delete all cache entries for a model (across all languages)
$this->pscache->delete_group('product_model');
// Delete a single entry
$this->pscache->delete($cacheFileName);
// Delete everything
$this->pscache->delete_all();Pscache Helper Functions
ecommercen/helpers/pscache_helper.php provides convenience functions:
php
// Clear cache for specific models
clearPsCacheArray(['product_model', 'product_category_model']);
// Clear by category
clearCache('products'); // product-related models
clearCache('settings'); // settings, registry, transporters
clearCache('seo'); // metatags, seo_lib
clearCache('blog'); // blog models
clearCache('all'); // everything
clearCache('cloudflare'); // Cloudflare CDN cache (not pscache)
// Cloudflare CDN purge
clearCloudflareCache();
// Convert mixed params to cache key string
getArrayToStringForPscache(['key' => 'value']); // '_key_value'Available categories: maps, cms, blog, sliders, promo, product_categories_vendors, settings, seo, products, search, product_bundles, cloudflare, all.
Configuration
Environment Variables
bash
# L2 — persistent, long-lived
APP_CACHE_L2_ADAPTER=Advisable\Cache\Adapter\FileAdapter # Adapter FQCN
APP_CACHE_L2_CONNECTION=../cache/ # Connection string
APP_CACHE_L2_PREFIX= # Tenant prefix (fallback)
APP_CACHE_L2_DEFAULT_EXPIRES=86400 # Default TTL (1 day)
APP_CACHE_L2_SEARCH_EXPIRES=43200 # Search TTL (12 hours)
APP_CACHE_L2_SEO_EXPIRES=864000 # SEO TTL (10 days)
APP_CACHE_L2_DELETE_ALL_USE_SCRIPT=true # Use OS script for bulk delete
APP_CACHE_L2_SIZE_LIMIT=5 # Size limit in GB
# L1 — server-local, cross-request
APP_CACHE_L1_ADAPTER=auto # 'auto', 'none', or FQCN
APP_CACHE_L1_CONNECTION= # Connection string
APP_CACHE_L1_PREFIX= # Tenant prefix (fallback)Config file: application/config/cache.php. In development (ENVIRONMENT=development), all L2 TTLs are set to -1 (caching disabled).
Example Configurations
Default (file-based L2, auto L1):
bash
# No env vars needed — defaults work out of the boxRedis L2 with tenant prefix:
bash
APP_CACHE_L2_ADAPTER=Advisable\Cache\Adapter\RedisAdapter
APP_CACHE_L2_CONNECTION=tcp://redis:6379?prefix=myshop&database=0Redis Cluster L2:
bash
APP_CACHE_L2_ADAPTER=Advisable\Cache\Adapter\RedisClusterAdapter
APP_CACHE_L2_CONNECTION=tcp://redis1:6379,tcp://redis2:6379,tcp://redis3:6379?prefix=myshopFile L2 with tenant isolation:
bash
APP_CACHE_L2_CONNECTION=../cache/?prefix=myshop
# Or equivalently:
APP_CACHE_L2_CONNECTION=../cache/
APP_CACHE_L2_PREFIX=myshopExplicit SHM for L1:
bash
APP_CACHE_L1_ADAPTER=Advisable\Cache\Adapter\ShmAdapter
APP_CACHE_L1_CONNECTION=/dev/shm/ecommercen?prefix=myshopL1 disabled:
bash
APP_CACHE_L1_ADAPTER=noneContainer Wiring
application/config/container/cache.php registers all adapters and aliases:
- L2: Alias
cache.l2points to the configured adapter FQCN.CachePoolis wired withcache.l2as its adapter. - L1: When
auto, aliascache.l1points toChainAdapterwhich receives[ApcuAdapter, ShmAdapter]as candidates with L2 as fallback. Whennone,cache.l1aliases tocache.l2. When an explicit FQCN, it's used directly. - L0: Always
InMemoryAdapter.
Testing
Test Structure
| Suite | Path | Adapters | Why |
|---|---|---|---|
| Unit | tests/Unit/Cache/ | InMemoryAdapter, ChainAdapter | Pure PHP — no OS syscalls or external services |
| Integration | tests/Integration/Cache/ | FileAdapter, ShmAdapter, ApcuAdapter, RedisAdapter | Require filesystem, shared memory, APCu extension, or a Redis server |
Running Tests
bash
# All cache tests (unit + integration)
vendor/bin/phpunit tests/Unit/Cache tests/Integration/Cache
# Unit only
vendor/bin/phpunit tests/Unit/Cache
# Integration only
vendor/bin/phpunit tests/Integration/Cache
# Single adapter
vendor/bin/phpunit --filter=RedisAdapterTestRunning in Docker
The recommended way to run the full test suite is via .docker/integration/. The PHP images there ship with all required extensions (APCu with apc.enable_cli=1, Redis) and the environment is preconfigured with a Redis backend, so every test runs with zero skips. From the .docker/integration/ directory, start the stack and run the tests:
bash
./compose.sh up -d
./compose.sh exec php-cli php vendor/bin/phpunit tests/Unit/Cache tests/Integration/Cache --no-coverageTEST_REDIS_HOST and TEST_REDIS_PORT are preconfigured in .docker/integration/.env.example to point at the Docker Redis container.
Extension-Gated Tests
- ApcuAdapterTest — requires the
apcuextension andapc.enable_cli=1. Uses#[RequiresPhpExtension('apcu')]and asetUp()guard for the INI setting. - RedisAdapterTest — requires the
redisextension and a reachable server. Uses#[RequiresPhpExtension('redis')]and skips viamarkTestSkipped()if the connection fails.
Tests that cannot connect to their backend are skipped, not failed — so the suite stays green on developer machines without Redis or APCu.
Test Isolation
Each adapter test creates isolated state to avoid cross-test contamination:
- File/SHM: Unique temp directories via
sys_get_temp_dir() . '/' . uniqid('', true), cleaned up intearDown(). - APCu: Unique key prefix per run via
'phpunit_' . uniqid('', true), cleared insetUp()/tearDown(). - Redis: Unique key prefix per run,
clear()insetUp()/tearDown(). - InMemory: Fresh instance per test in
setUp(). - Chain: Mock adapters — no shared state.
File Locations
| Component | Path |
|---|---|
| CacheAdapterInterface | src/Cache/Adapter/CacheAdapterInterface.php |
| FileAdapter | src/Cache/Adapter/FileAdapter.php |
| RedisAdapter | src/Cache/Adapter/RedisAdapter.php |
| RedisClusterAdapter | src/Cache/Adapter/RedisClusterAdapter.php |
| ApcuAdapter | src/Cache/Adapter/ApcuAdapter.php |
| ShmAdapter | src/Cache/Adapter/ShmAdapter.php |
| InMemoryAdapter | src/Cache/Adapter/InMemoryAdapter.php |
| ChainAdapter | src/Cache/Adapter/ChainAdapter.php |
| CachePool | src/Cache/CachePool.php |
| CacheItem | src/Cache/CacheItem.php |
| GroupableCacheInterface | src/Cache/GroupableCacheInterface.php |
| Pscache | application/libraries/Pscache.php |
| Cache config | application/config/cache.php |
| Container config | application/config/container/cache.php |
| Pscache helper | ecommercen/helpers/pscache_helper.php |
| Key extraction trait | src/Cache/Adapter/WithKeyPartsExtractionTrait.php |
| Redis key trait | src/Cache/Adapter/WithRedisKeyParsingTrait.php |
| Redis options trait | src/Cache/Adapter/WithRedisOptionsTrait.php |