Configuration
A config describes one endpoint: where to go, what to authenticate with, which database to select. The pool builds every one of its connections from it, so anything that must be identical on every connection lives here.
The config class
Extend RedisConfig and fill it in inside setUp():
<?php
namespace Main\Configurations;
use Flytachi\Winter\Redis\Config\RedisConfig;
class MainRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->port = (int) env('REDIS_PORT', 6379);
$this->password = env('REDIS_PASS', '');
}
}setUp() runs once per instance created — that is, once per connection in the pool.
Reading the environment here is fine; talking to a database or the network is not.
What can be set
| Property | Default | What it is |
|---|---|---|
$host |
'localhost' |
server address; a scheme is allowed (tls://, unix://) |
$port |
6379 |
port |
$username |
'' |
ACL user (Redis 6+); empty means the default user |
$password |
'' |
password; an empty string means no AUTH is sent |
$databaseIndex |
0 |
database index (SELECT) |
$timeout |
1.5 |
seconds to open the socket |
$readTimeout |
2.0 |
seconds to wait for a reply once connected |
$serializer |
Redis::SERIALIZER_NONE |
how values are encoded |
$context |
[] |
stream context: CA, client certificate, peer verification |
Two timeouts, and they mean different things
They get confused more than anything else here, and their consequences differ.
$timeout is how long to wait for the connection to be established. Exceeded, the
connection never opened, and the pool raises a PoolException that surfaces as
RedisPoolException. This one is about an unreachable server.
$readTimeout is how long to wait for a reply to a command already sent. Exceeded,
you get RedisException: read error on connection, which reads as a dropped connection
rather than a timeout. This one is about a server that took the command and went quiet.
$this->timeout = 1.5; // cannot reach it — fail fast
$this->readTimeout = 2.0; // no reply for two seconds — call the link lostreadTimeout and blocking commands
BLPOP, BRPOP and SUBSCRIBE make the server go quiet on purpose — until data
arrives. To the connection that is indistinguishable from a hung server, so
consume(timeout: 5) on a connection with readTimeout = 2.0 dies on the second second
with a read error. Measured.
The list handle accounts for that and raises the read timeout for the wait itself — but
if you write a blocking command through raw(), raise it yourself:
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1) removes the limit (it must be -1; 0
means “do not wait at all”).
Both values apply to one connection, not to a request: the pool may spend longer
waiting for a free connection, and that is the separate $poolWaitTimeout.
Authentication
A password is enough for an ordinary installation — it authenticates the default user:
$this->password = env('REDIS_PASS', '');Redis 6 added ACLs: separate users with their own permissions and their own reachable
keys. Managed Redis offerings often hand out such a user rather than the default
password:
$this->username = env('REDIS_USER', 'app');
$this->password = env('REDIS_PASS');With $username empty, AUTH <password> is sent; with it set, AUTH <user> <password>.
To check who you connected as:
$config->connection()->rawCommand('ACL', 'WHOAMI'); // 'app'The password never reaches the package's logs
getDsn() is deliberately built from host, port and database — neither password nor user
appears in it, so the string is safe to log whole. That is what the pool logs.
TLS and unix sockets
The scheme goes into the address itself; the driver parses it:
$this->host = 'tls://redis.example.com';
$this->port = 6380;TLS material — a trusted CA, a client certificate, peer verification requirements —
travels in $context:
$this->host = 'tls://redis.example.com';
$this->port = 6380;
$this->context = [
'stream' => [
'cafile' => '/etc/ssl/certs/redis-ca.pem',
'verify_peer' => true,
],
];For a local socket:
$this->host = '/var/run/redis/redis.sock';
$this->port = 0; // the port is unusedTLS failures arrive as warnings, not exceptions
The driver reports a failed handshake with a PHP warning (Failed to enable crypto), and
the connection may still be considered established. Verify the setup with an explicit
ping() on the first deploy, not by the absence of exceptions.
Several configs
A config is not “the Redis settings of the application” but one endpoint. Having several is normal:
class MainRedisConfig extends RedisConfig { /* database 0 — sessions, cache */ }
class QueueRedisConfig extends RedisConfig { /* database 1 — queues */ }
class MetricsRedisConfig extends RedisConfig { /* a different server entirely */ }Each gets its own pool with its own ceiling, and they appear separately in
RedisPool::stats(). Stores attach to the one they need through
$redisConfigClassName.
Splitting pays off when the parts have different needs: a queue with blocking readers should not compete for connections with a hot cache, and metrics should not take the application down when their server is unreachable.
The database index
It is set in the config and only there:
class QueueRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->databaseIndex = 3;
}
}A second database means a second config class. It gets its own pool with its own connections, so the two cannot be confused by construction.
Never switch databases at runtime
SELECT changes the state of the connection, and the connection goes back to the pool.
The next request would receive it along with someone else’s database and write to the
wrong place — silently, without a single error. That is why nothing resembling
select() exists in the store API: changing database means changing config.
The selected database also survives a dropped connection: ext-redis reconnects
transparently and restores AUTH and SELECT itself, so a connection never wakes up
on database zero.
Value serialization
The default is SERIALIZER_NONE: exactly the bytes you pass go to the server. That is
the only form redis-cli and applications in other languages can read.
The price is that a value must be a string or a number:
$store->set('user', ['id' => 1]); // → the string "Array" in the database, with only a PHP warning
$store->get('user'); // 'Array'Nothing throws — which is why the serializer is a property of the config, where the choice is made once and in plain sight:
use Redis;
class CacheRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->serializer = Redis::SERIALIZER_PHP; // arrays and objects as they are
}
}| Value | What it gives | What it costs |
|---|---|---|
SERIALIZER_NONE |
bytes as they are, readable by everything | strings and numbers only |
SERIALIZER_PHP |
any PHP value, objects included | only PHP can read the format |
SERIALIZER_JSON |
any array, readable by other languages | objects come back as stdClass |
Change the serializer before the database holds data
It applies to the connection, not to the key, so values written earlier stop being readable afterwards: the old bytes are decoded the new way. On a populated database this is a migration, not a setting.
Pool size
By default a config gets 10 connections and waits 3 seconds for a free one. To choose your own, let the config declare itself pool-aware:
<?php
namespace Main\Configurations;
use Flytachi\Winter\Redis\Config\RedisConfig;
use Flytachi\Winter\Redis\Pool\RedisPoolConfigInterface;
use Flytachi\Winter\Redis\Pool\RedisPoolTrait;
class MainRedisConfig extends RedisConfig implements RedisPoolConfigInterface
{
use RedisPoolTrait;
public int $poolMaxConnections = 20;
public float $poolWaitTimeout = 5.0;
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
}
}The trait supplies every method of the interface with a default, so you declare only what you change. Each setting is discussed in the Connection pool.
A one-off connection
For a script, a migration or a test there is no reason to declare a class — take the inline variant:
<?php
require 'vendor/autoload.php';
use Flytachi\Winter\Redis\Config\Call\RedisCall;
$redis = (new RedisCall(
host: '127.0.0.1',
port: 6379,
databaseIndex: 3,
))->connection();
$redis->set('warm', '1');Each such object opens its own connection and closes it when it is garbage-collected. In an application serving requests that is the wrong shape — there it is a config and a pool.
Diagnostics
use Flytachi\Winter\Redis\RedisPool;
$config = RedisPool::config(MainRedisConfig::class);
$config->getDsn(); // 'redis://localhost:6379/0' — no password, safe to log
$config->ping(); // true | false, never throws
$config->pingDetail(); // ['status' => true, 'latency' => 0.32, 'error' => null]pingDetail() is what /actuator/health reports: it answers not only “is it alive” but
how long the round trip took, and on failure the error text.
The instance from config() is not the one in the pool
RedisPool::config() returns the registration instance: it exists so settings can be
read. The connections are held by other instances — one per pool slot, and those are the
ones the pool probes and closes.
Next
- Stores — how application code uses all this
- Connection pool — how many connections, and what happens when they run out