Standalone package — The cache adapter system lives in
nations-original/php-sf-cache, a separate Composer package with no dependency on the PHP-SF framework. You can use it in any PHP project — Symfony apps, legacy codebases, or custom frameworks — without pulling in the rest of the framework.
The package provides four backends — Redis, APCu, Memcached, and the filesystem — all implementing the same PSR-16 CacheInterface. The ca() helper auto-selects the best available adapter; rca(), aca(), mca(), and fca() give direct access to specific backends.
composer require nations-original/php-sf-cache
Wire connections and adapters in services.yaml. Each connection is a proper constructor-injected service; adapters receive their connection as an argument so the container owns the full wiring:
PHP_SF\Cache\Connection\Redis:
arguments:
- '%env(REDIS_CACHE_URL)%'
- '%env(SERVER_PREFIX)%:%env(APP_ENV)%:'
PHP_SF\Cache\Connection\Memcached:
arguments:
- '%env(MEMCACHED_SERVER)%'
- '%env(int:MEMCACHED_PORT)%'
- '%env(SERVER_PREFIX)%:%env(APP_ENV)%:'
PHP_SF\Cache\Adapter\RedisCacheAdapter:
arguments:
- '@PHP_SF\Cache\Connection\Redis'
PHP_SF\Cache\Adapter\MemcachedCacheAdapter:
arguments:
- '@PHP_SF\Cache\Connection\Memcached'
PHP_SF\Cache\Adapter\FileSystemCacheAdapter:
arguments:
$filesystem: '@filesystem'
$cacheDir: '%kernel.cache_dir%/php_sf_cache'
To use an adapter as a PSR-16 service, bind it to Psr\SimpleCache\CacheInterface:
Psr\SimpleCache\CacheInterface: '@PHP_SF\Cache\Adapter\RedisCacheAdapter'
Or wrap any adapter as a PSR-6 pool:
$pool = new Symfony\Component\Cache\Psr16Cache( rca() );
The service provider is auto-discovered — no registration needed. It reads from Laravel's existing config:
| Adapter | Laravel config source |
|---|---|
| Redis | config/database.php → redis.cache (fallback: redis.default) |
| Memcached | config/cache.php → stores.memcached.servers[0] |
| FileSystem | storage/framework/cache/php_sf_cache |
Key prefix is built automatically as {APP_NAME}:{APP_ENV}:.
By default PHP_SF\Cache\CacheInterface is bound to FileSystemCacheAdapter. Override in AppServiceProvider to restore the previous behaviour or pin to a specific backend:
use PHP_SF\Cache\CacheInterface;
public function register(): void
{
$this->app->singleton( CacheInterface::class, fn() => rca() );
}
Set env vars in .env — the package reads $_ENV directly, no env() call needed:
REDIS_CACHE_URL=redis://localhost:6379/0
SERVER_PREFIX=myapp
APP_ENV=prod
MEMCACHED_SERVER=localhost
MEMCACHED_PORT=11211
CACHE_DIR=/var/www/myapp/var/cache/php_sf_cache
The framework's docker-compose.yml starts Redis on port 7002 and Memcached on port 7001.
Call helpers after require 'vendor/autoload.php'. Set $_ENV before the first call, or use configure() explicitly:
require 'vendor/autoload.php';
// Via $_ENV
$_ENV['REDIS_CACHE_URL'] = 'redis://127.0.0.1:6379/0';
// Or explicitly
use PHP_SF\Cache\Connection\Redis;
use PHP_SF\Cache\Adapter\FileSystemCacheAdapter;
use Symfony\Component\Filesystem\Filesystem;
Redis::configure( 'redis://127.0.0.1:6379/0', 'myapp:prod:' );
FileSystemCacheAdapter::configure( new Filesystem(), '/tmp/myapp_cache' );
| Adapter | Helper | Scope | Pattern deletion | Accepts | Requires |
|---|---|---|---|---|---|
| APCu | aca() |
Per-process | Yes | Scalar | ext-apcu |
| Redis | rca() |
Shared | Yes | Scalar | Redis server |
| Memcached | mca() |
Shared | No | Scalar | Memcached server |
| FileSystem | fca() |
Local disk | Yes | Scalar, array, object | symfony/filesystem |
APCu stores data in the PHP process memory. No network round trip — reads are as fast as a PHP array lookup. The trade-off is that data is local to the current process and invisible to other servers. In DEV_MODE, APCu is cleared on every request boot.
Redis is the primary shared cache. Data is visible across all application servers, survives process restarts, and supports pattern-based key deletion and pub/sub. See Redis & Pub/Sub for full documentation.
Memcached is an alternative shared cache. Similar to Redis for basic get/set but without pattern deletion, pub/sub, or sorted sets. Use when you already have Memcached in your stack.
FileSystem stores serialized data in files on local disk. Accepts scalar, array, and object values — no need to j_encode() before storing. Survives process restarts. Best for heavy computed objects or environments where Redis and APCu are not available. Not shared across servers.
Every adapter exposes a static isAvailable() method that checks whether the backend is reachable without throwing:
use PHP_SF\Cache\Adapter\RedisCacheAdapter;
use PHP_SF\Cache\Adapter\MemcachedCacheAdapter;
use PHP_SF\Cache\Adapter\APCuCacheAdapter;
use PHP_SF\Cache\Adapter\FileSystemCacheAdapter;
RedisCacheAdapter::isAvailable(); // TCP check
MemcachedCacheAdapter::isAvailable(); // ext-memcached loaded + TCP check
APCuCacheAdapter::isAvailable(); // apcu_enabled()
FileSystemCacheAdapter::isAvailable(); // always true
Use it in health-check routes, test setUp(), or any conditional logic before calling the adapter:
if ( RedisCacheAdapter::isAvailable() )
rca()->set( 'key', $value );
ca( string|null $cacheAdapter = null ): AbstractCacheAdapter
Returns the best available adapter. With no argument, walks the fallback chain: APCu → Redis → Memcached → FileSystem, returning the first available backend:
ca()->set( 'player:123:score', 4500, 3600 );
ca()->get( 'player:123:score' );
ca()->has( 'player:123:score' );
ca()->delete( 'player:123:score' );
Force a specific adapter using the class constants:
use PHP_SF\Cache\Abstracts\AbstractCacheAdapter;
ca( AbstractCacheAdapter::REDIS_CACHE_ADAPTER )->set( 'key', $value );
ca( AbstractCacheAdapter::APCU_CACHE_ADAPTER )->set( 'key', $value );
ca( AbstractCacheAdapter::MEMCACHED_CACHE_ADAPTER )->set( 'key', $value );
ca( AbstractCacheAdapter::FILESYSTEM_CACHE_ADAPTER )->set( 'key', $value );
ca() is the right default for most application cache use cases. Use specific adapters directly only when you need backend-specific features.
All four adapters implement the same CacheInterface (which extends Psr\SimpleCache\CacheInterface):
get( string $key, mixed $default = null ): mixed
Returns the cached value or $default if the key doesn't exist or has expired:
$score = ca()->get( 'player:123:score' ); // null if missing
$score = ca()->get( 'player:123:score', 0 ); // 0 if missing
$data = ca()->get( 'config:features', '{}' ); // '{}' if missing
set( string $key, mixed $value, DateInterval|int|null $ttl = 86400 ): bool
Default TTL is 86400 seconds (24 hours). Pass null explicitly for no expiry.
Stores a value with optional TTL in seconds.
Redis, APCu, Memcached — only scalar values are accepted. Arrays, objects, and other non-scalar types throw CacheValueException. Serialize before storing:
ca()->set( 'player:123:score', 4500, 3600 );
ca()->set( 'player:123:profile', j_encode( $player->jsonSerialize() ), 3600 );
ca()->set( 'session:token', $token, new DateInterval( 'PT30M' ) );
ca()->set( 'config:map_size', 512, null ); // no expiry
FileSystem — accepts scalar, array, and object values directly:
fca()->set( 'player:123:profile', $playerObject, 3600 );
fca()->set( 'report:heavy', [ 'rows' => $rows, 'totals' => $totals ], 600 );
Returns true if stored successfully, false otherwise.
has( string $key ): bool
Returns true if the key exists and hasn't expired:
if ( ca()->has( 'player:123:profile' ) === false )
// rebuild and cache
delete( string $key ): bool
ca()->delete( 'player:123:score' );
clear(): bool
Wipes all keys in the cache. For Redis this flushes the entire database. Use with caution in production:
rca()->clear();
aca()->clear();
mca()->clear();
fca()->clear();
getMultiple( iterable $keys, mixed $default = null ): iterable
setMultiple( iterable $values, DateInterval|int|null $ttl = 86400 ): bool
deleteMultiple( iterable $keys ): bool
ca()->setMultiple( [
'player:1:score' => 4500,
'player:2:score' => 3200,
], 3600 );
$values = ca()->getMultiple( [ 'player:1:score', 'player:2:score' ] );
ca()->deleteMultiple( [ 'player:123:score', 'player:123:profile' ] );
setMultiple() on Redis/APCu/Memcached requires scalar values. On FileSystem, all serializable types are accepted.
Note: getMultiple() is implemented as individual get() calls in a loop. For bulk Redis reads, use rc()->mget() directly.
deleteByKeyPattern( string $keyPattern ): bool
Deletes all keys matching a pattern. Available on Redis, APCu, and FileSystem. Not supported by Memcached.
Returns true when no keys match the pattern (nothing to delete is treated as success). Returns false only when a deletion attempt fails.
The * wildcard is supported at the very start, end, or both ends of the pattern. Any * that is not in position 0 or the last position throws InvalidCacheKeyException:
rca()->deleteByKeyPattern( 'player:123:*' ); // prefix — valid
rca()->deleteByKeyPattern( '*:score' ); // suffix — valid
rca()->deleteByKeyPattern( '*player*' ); // contains — valid
rca()->deleteByKeyPattern( '*' ); // everything — valid
// Throws — * not at start or end
rca()->deleteByKeyPattern( 'player:*:score' ); // middle
rca()->deleteByKeyPattern( '*player:*:score' ); // start + middle
Keys are matched against the unprefixed key name — the SERVER_PREFIX:APP_ENV: prefix is stripped automatically.
The FileSystem adapter matches against the original key string stored in the cache file. Scanning is O(n) in the number of cached files — avoid on very large filesystem caches.
Calling mca()->deleteByKeyPattern() always throws UnsupportedPlatformException. Use rca() or fca() when pattern deletion is needed.
rca()->pub( string $channel, mixed $message ): bool // pub/sub publish
rc() // raw Predis\Client — for MGET, pipeline, Lua scripts, etc.
rp() // Predis\Pipeline
See Redis & Pub/Sub for full documentation.
Colon-separated namespacing is the convention used throughout the project:
player:{id}:profile
player:{id}:inventory
guild:{id}:members
leaderboard:global:top100
The full stored key includes the SERVER_PREFIX:APP_ENV: prefix — a key stored as player:123:score appears in Redis as myapp:prod:player:123:score.
| Data type | Suggested TTL | Reasoning |
|---|---|---|
| Route list | None | Only changes on deployment |
| Template classes | None | Only changes on deployment |
| Translation strings | None | Only changes on deployment |
| Player profile | 3600 (1 hour) | Changes infrequently |
| Leaderboard | 300 (5 min) | Acceptable staleness |
| Session data | 1800 (30 min) | Active session window |
| Temporary computation | 60–300 | Short-lived results |
| Rate limit counters | 60 | Per-minute window |
| Heavy computed objects (filesystem) | 600–3600 | Expensive to rebuild |
Use null TTL only for data that should persist until explicitly cleared — route lists, compiled templates, configuration. Never use null for player data or anything user-specific.
Entities must be serialized before caching with Redis, APCu, and Memcached since those adapters only accept scalar values:
// Store
ca()->set(
sprintf( 'player:%d:profile', $player->getId() ),
j_encode( $player->jsonSerialize() ),
3600
);
// Retrieve
$cached = ca()->get( sprintf( 'player:%d:profile', $playerId ) );
if ( $cached !== null )
$data = j_decode( $cached, true );
With the filesystem adapter you can store the object directly:
fca()->set( sprintf( 'player:%d:profile', $player->getId() ), $player, 3600 );
$player = fca()->get( sprintf( 'player:%d:profile', $playerId ) );
APCu as L1 and Redis as L2 gives the fastest possible reads for hot data:
function getPlayerScore( int $playerId ): int
{
$key = sprintf( 'player:%d:score', $playerId );
if ( function_exists( 'apcu_enabled' ) && apcu_enabled() ) {
$score = aca()->get( $key );
if ( $score !== null )
return (int) $score;
}
$score = rca()->get( $key );
if ( $score !== null ) {
if ( function_exists( 'apcu_enabled' ) && apcu_enabled() )
aca()->set( $key, $score, 300 );
return (int) $score;
}
$score = Player::find( $playerId )->getScore();
rca()->set( $key, $score, 3600 );
if ( function_exists( 'apcu_enabled' ) && apcu_enabled() )
aca()->set( $key, $score, 300 );
return $score;
}
rca()->deleteByKeyPattern( sprintf( 'player:%d:*', $player->getId() ) );
AbstractEntity does this automatically on every postPersist, postUpdate, and postRemove via clearQueryBuilderCache().
For data where some staleness is acceptable — leaderboards, aggregate statistics. Set a TTL and let it expire naturally.
For data with exactly one consumer:
$data = ca()->get( $key );
ca()->delete( $key );
// use $data
All adapters extend AbstractCacheAdapter which provides getMultiple(), setMultiple(), and deleteMultiple() as default implementations. Each concrete adapter implements:
get(), set(), delete(), clear(), has(), deleteByKeyPattern()All adapters have a public constructor so DI containers (Symfony, Laravel) can instantiate them directly. The getInstance() singleton is the recommended path outside of DI — both coexist safely. FileSystemCacheAdapter::configure() and Redis::configure() / Memcached::configure() allow explicit setup before the first call.
Storing non-scalar values with Redis/APCu/Memcached — throws CacheValueException. Serialize first, or switch to fca():
// Wrong
ca()->set( 'data', [ 'key' => 'value' ] );
// Correct (Redis/APCu/Memcached)
ca()->set( 'data', j_encode( [ 'key' => 'value' ] ) );
// Correct (filesystem — no serialization needed)
fca()->set( 'data', [ 'key' => 'value' ] );
Using mca()->deleteByKeyPattern() — always throws UnsupportedPlatformException. Use rca() or fca().
Assuming ca() always returns the same adapter — ca() walks APCu → Redis → Memcached → FileSystem and returns the first available backend. If you store via ca() on one server and read via rca() on another, you may get a cache miss because the data was stored in a different backend. Use the same adapter consistently for the same key.
All scalar values returned as strings from Redis — Predis stores everything as strings. Integers, floats, and booleans all come back as strings. Cast explicitly on retrieval:
$count = (int) rca()->get( 'hits' );
$ratio = (float) rca()->get( 'ratio' );
$enabled = (bool) rca()->get( 'feature:enabled' ); // '' = false, '1' = true
Pattern with * not at start or end — deleteByKeyPattern( 'player:*:score' ) throws InvalidCacheKeyException. Patterns like *player:*:score (start + middle) are also rejected. Restructure your key naming so related keys share a common prefix or suffix.
Forgetting the key prefix in redis-cli — a key stored as player:123:score appears in Redis as myapp:prod:player:123:score. Use KEYS *player:123:score to find it.