Redis is the only required external service in the framework. It serves multiple roles simultaneously — application cache, route cache, translation cache, template cache, and real-time pub/sub. This page covers the Redis connection, available helper functions, the pipeline system, and pub/sub usage.
REDIS_CACHE_URL=redis://localhost:6379/0
The URL format is redis://host:port/database. The database index (0–15) determines which Redis logical database is used. Using a dedicated database index per environment is recommended to avoid key collisions between dev and prod on the same Redis instance.
The connection is handled by PHP_SF\System\Database\Redis using the predis/predis client. It is a singleton — the connection is opened once on first use and reused for all subsequent operations within a request.
All Redis keys are automatically prefixed with:
{SERVER_PREFIX}:{APP_ENV}:
So if SERVER_PREFIX=my-app and APP_ENV=dev, a key stored as user:123 is actually stored in Redis as my-app:dev:user:123.
This means you never need to manually namespace keys — the prefix handles environment and server isolation automatically. It also means when you use KEYS * in redis-cli, you'll see the full prefixed key names.
When deleting by pattern via rca()->deleteByKeyPattern(), use the unprefixed key pattern — the method strips the prefix internally before matching.
Four global functions provide access to Redis:
rc() — raw Predis clientrc(): Predis\Client
Returns the raw Predis client instance. Use this when you need Redis commands not covered by the cache adapter — sorted sets, lists, pub/sub subscriptions, key TTL checks, etc.
// Check if a key exists
rc()->exists( 'my_key' );
// Get TTL of a key
rc()->ttl( 'my_key' );
// Increment a counter
rc()->incr( 'page_views' );
// Push to a list
rc()->rpush( 'event_log', [ json_encode( $event ) ] );
// Get a range from a list
rc()->lrange( 'event_log', 0, -1 );
// Add to a sorted set
rc()->zadd( 'leaderboard', [ 'player:123' => 4500 ] );
// Get top 10 from sorted set
rc()->zrevrange( 'leaderboard', 0, 9, [ 'WITHSCORES' => true ] );
rp() — Redis pipelinerp(): Predis\Pipeline\Pipeline
Returns the Redis pipeline instance. A pipeline batches multiple commands and sends them to Redis in a single round trip, significantly reducing latency for write-heavy operations.
Commands queued on the pipeline are not executed immediately — they are buffered and sent all at once when rp()->execute() is called. This happens automatically at the end of every request via a shutdown function registered in the framework kernel:
// Registered automatically in PHP_SF\System\Kernel constructor
register_shutdown_function( function () {
rp()->execute();
} );
This means you can queue pipeline commands anywhere in the request cycle without worrying about when they execute — they always flush at the end.
// Queue multiple writes — sent in one round trip at request end
rp()->set( 'player:123:last_seen', time() );
rp()->incr( 'player:123:session_count' );
rp()->rpush( 'server:active_players', [ '123' ] );
The footer template in the default layout uses the pipeline for page load time tracking:
rp()->rpush( $key, [ $currentPageLoadTime ] );
rp()->expire( $key, 86400 );
Do not use the pipeline for reads — pipelined commands return responses only after execute(), so you can't read a value you just queued. Use rc() or rca() for reads.
rca() — Redis cache adapterrca(): PHP_SF\System\Core\Cache\RedisCacheAdapter
Returns the Redis cache adapter, which implements PSR-16 CacheInterface. Use this for standard get/set/delete cache operations with TTL support. This is the most commonly used Redis helper.
// Store a value for 1 hour
rca()->set( 'player:123:profile', json_encode( $profile ), 3600 );
// Retrieve a value
$profile = rca()->get( 'player:123:profile' );
// Check existence
rca()->has( 'player:123:profile' );
// Delete a key
rca()->delete( 'player:123:profile' );
// Delete by pattern
rca()->deleteByKeyPattern( 'player:123:*' );
// Clear entire database
rca()->clear();
TTL is in seconds. Pass null to store without expiry.
The cache adapter is also available via ca() which auto-selects between APCu and Redis depending on what's available — see Cache for details on the full cache system.
ca() — unified cache adapterca(): PHP_SF\System\Classes\Abstracts\AbstractCacheAdapter
Returns APCu adapter if APCu is available and enabled, Redis adapter otherwise. For most application cache use cases, prefer ca() over rca() directly — it automatically uses the fastest available cache layer.
Use rca() directly only when you specifically need Redis features like pub/sub, pipelines, or pattern deletion that APCu doesn't support.
The Redis cache adapter exposes a pub() method for publishing messages to a Redis channel:
rca()->pub( string $channel, mixed $message ): bool
Non-scalar messages are automatically JSON-encoded before publishing.
// Publish a game event to a channel
rca()->pub( 'game:events', [
'type' => 'unit_moved',
'user_id' => 123,
'from' => [ 'x' => 10, 'y' => 5 ],
'to' => [ 'x' => 11, 'y' => 5 ],
] );
// Publish a scalar notification
rca()->pub( 'notifications:user:123', 'You have been attacked!' );
Subscriptions are blocking operations, like RabbitMQ consumers — they belong in long-running console commands, not HTTP requests. Use the raw Predis client via rc() to subscribe:
// App/Command/GameEventListenerCommand.php
#[AsCommand(
name: 'app:events:listen',
description: 'Listen for real-time game events',
)]
final class GameEventListenerCommand extends Command
{
protected function execute( InputInterface $input, OutputInterface $output ): int
{
$io = new SymfonyStyle( $input, $output );
// Subscribe blocks indefinitely — must run in a separate process
rc()->subscribe( 'game:events', function ( $redis, $channel, $message ) use ( $io ): void {
$event = json_decode( $message, true );
$io->text( sprintf(
'[%s] %s on channel %s',
date( 'H:i:s' ),
$event['type'],
$channel
) );
// handle the event
} );
return Command::SUCCESS;
}
}
To subscribe to multiple channels matching a pattern:
rc()->psubscribe( 'notifications:user:*', function ( $redis, $pattern, $channel, $message ): void {
$userId = str_replace( 'notifications:user:', '', $channel );
// handle notification for $userId
} );
The framework itself uses Redis for several internal purposes. Understanding these helps avoid key collisions and explains what you'll see in redis-cli:
| Key pattern | Used for | TTL |
|---|---|---|
cache:routes_list |
Serialized route list | None (cleared on app:cache:clear) |
cache:routes_by_url_list |
Routes indexed by URL | None |
parsed_url:{method}:{hash} |
Per-URL route resolution cache | None |
cached_template_class_{class} |
Compiled template classes | None |
translated_strings:{locale} |
Translation strings per locale | None |
:GET:{url}:{id} |
Redirect GET data | 300 seconds |
:POST:{url}:{id} |
Redirect POST data | 300 seconds |
:ERRORS:{url}:{id} |
Redirect error messages | 300 seconds |
:MESSAGES:{url}:{id} |
Redirect flash messages | 300 seconds |
:FORM_DATA:{url}:{id} |
Redirect form data | 300 seconds |
The redirect keys (5 second TTL) are the mechanism behind the framework's server-side redirect system — see Sessions & Redirects for details.
# Via console command (clears all cache adapters)
php bin/console app:cache:clear
# Via API endpoint (from an allowed host)
GET /api/cache_clear/all
# Directly via redis-cli (nuclear option)
redis-cli -p 7002 FLUSHDB
app:cache:clear calls rca()->clear() which flushes the entire Redis database. This clears everything — routes, templates, translations, and application cache — so the next request will be slower as caches are rebuilt.
Not accounting for key prefix in redis-cli — if you store a key as user:123 but can't find it in redis-cli with GET user:123, it's because the key is actually stored as my-app:dev:user:123. Use KEYS *user:123* to find it.
Sharing a Redis database index between environments — if dev and prod both use database 0, cache clears in dev will wipe prod data. Use different database indexes or different SERVER_PREFIX values per environment.
Publishing non-serializable data — pub() calls json_encode() on non-scalar values. Objects that aren't JSON-serializable will result in false being published. Make sure your payloads contain only JSON-compatible types.