Skip to content

<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

TierAliasDefault AdapterLifetimePurpose
L0cache.l0InMemoryAdapterSingle requestDedup within one HTTP request
L1cache.l1ChainAdapter (auto)Server-local, cross-requestHot data (categories, settings)
L2cache.l2FileAdapterPersistent (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): Uses ChainAdapter to try APCu first, then SHM, falling back to L2 if neither is healthy.
  • none: Disabled; cache.l1 becomes an alias for cache.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');  // InMemoryAdapter

Adapters

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.abc becomes cache/user/profile/&lt;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.sh on Linux, cache/delete.bat on 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/) to del/ first (atomic, zero disk space cost), then creates a fresh empty replacement directory (only ~4 KB), then deletes del/. 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 stale del/ directory remains on disk. Without cleanup, a subsequent mv el del would move el/ inside the existing del/ subtree instead of replacing it, causing unpredictable cache state. The script removes any leftover del/ at the very start of each run with rm -rf del 2>/dev/null to prevent this.
  • 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.abc becomes prefix: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=0 or 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() checks apcu_enabled().

ShmAdapter

  • Storage: /dev/shm filesystem (Linux shared memory)
  • Key mapping: MD5-hashed filenames as .shm files.
  • 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=tenant

Prefix 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 everything

Key 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 box

Redis L2 with tenant prefix:

bash
APP_CACHE_L2_ADAPTER=Advisable\Cache\Adapter\RedisAdapter
APP_CACHE_L2_CONNECTION=tcp://redis:6379?prefix=myshop&database=0

Redis 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=myshop

File L2 with tenant isolation:

bash
APP_CACHE_L2_CONNECTION=../cache/?prefix=myshop
# Or equivalently:
APP_CACHE_L2_CONNECTION=../cache/
APP_CACHE_L2_PREFIX=myshop

Explicit SHM for L1:

bash
APP_CACHE_L1_ADAPTER=Advisable\Cache\Adapter\ShmAdapter
APP_CACHE_L1_CONNECTION=/dev/shm/ecommercen?prefix=myshop

L1 disabled:

bash
APP_CACHE_L1_ADAPTER=none

Container Wiring

application/config/container/cache.php registers all adapters and aliases:

  • L2: Alias cache.l2 points to the configured adapter FQCN. CachePool is wired with cache.l2 as its adapter.
  • L1: When auto, alias cache.l1 points to ChainAdapter which receives [ApcuAdapter, ShmAdapter] as candidates with L2 as fallback. When none, cache.l1 aliases to cache.l2. When an explicit FQCN, it's used directly.
  • L0: Always InMemoryAdapter.

Testing

Test Structure

SuitePathAdaptersWhy
Unittests/Unit/Cache/InMemoryAdapter, ChainAdapterPure PHP — no OS syscalls or external services
Integrationtests/Integration/Cache/FileAdapter, ShmAdapter, ApcuAdapter, RedisAdapterRequire 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=RedisAdapterTest

Running 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-coverage

TEST_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 apcu extension and apc.enable_cli=1. Uses #[RequiresPhpExtension('apcu')] and a setUp() guard for the INI setting.
  • RedisAdapterTest — requires the redis extension and a reachable server. Uses #[RequiresPhpExtension('redis')] and skips via markTestSkipped() 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 in tearDown().
  • APCu: Unique key prefix per run via 'phpunit_' . uniqid('', true), cleared in setUp()/tearDown().
  • Redis: Unique key prefix per run, clear() in setUp()/tearDown().
  • InMemory: Fresh instance per test in setUp().
  • Chain: Mock adapters — no shared state.

File Locations

ComponentPath
CacheAdapterInterfacesrc/Cache/Adapter/CacheAdapterInterface.php
FileAdaptersrc/Cache/Adapter/FileAdapter.php
RedisAdaptersrc/Cache/Adapter/RedisAdapter.php
RedisClusterAdaptersrc/Cache/Adapter/RedisClusterAdapter.php
ApcuAdaptersrc/Cache/Adapter/ApcuAdapter.php
ShmAdaptersrc/Cache/Adapter/ShmAdapter.php
InMemoryAdaptersrc/Cache/Adapter/InMemoryAdapter.php
ChainAdaptersrc/Cache/Adapter/ChainAdapter.php
CachePoolsrc/Cache/CachePool.php
CacheItemsrc/Cache/CacheItem.php
GroupableCacheInterfacesrc/Cache/GroupableCacheInterface.php
Pscacheapplication/libraries/Pscache.php
Cache configapplication/config/cache.php
Container configapplication/config/container/cache.php
Pscache helperecommercen/helpers/pscache_helper.php
Key extraction traitsrc/Cache/Adapter/WithKeyPartsExtractionTrait.php
Redis key traitsrc/Cache/Adapter/WithRedisKeyParsingTrait.php
Redis options traitsrc/Cache/Adapter/WithRedisOptionsTrait.php