Redis
Redis usually enters an application as “put a value in, take it out”. The difficulty does not start with the commands but with the connection: there is one of it, it lives for weeks, and in a resident worker it cannot be given to two requests at once. winter-redis handles exactly that — a pool owns the connections, and application code sees a store with plain methods.
Why this exists
The problem. A Redis connection is a single socket with a sequential protocol: command → reply → next command. While one request is using it, a second cannot get through. In classic PHP that never mattered: a process served one request and died. A resident Winter worker serves many requests at once, each in its own coroutine — and one shared connection there means two coroutines writing into it interleaved. A reply arrives for someone else’s request, the log fills with “packets out of order”, and under load the worker dies outright.
Opening a connection per request is the other extreme: a handshake, authentication and a
database selection every time, and a thousand concurrent requests become a thousand
connections and max number of clients reached on the server.
The answer. A set of ready connections, from which a request takes one for the
duration of its work and returns it when done. Connections are reused, their number is
capped, and dead ones are replaced — which is what winter-redis does on top of
CPool.
A small application may not need any of this
One connection per request through #[Bean(scope: Scope::Request)] is correct and
sufficient for many projects, and it is described in
Basic connections. A pool is for when its price
becomes visible: reopening on every request, and an unbounded number of connections.
What it is made of
Three layers, each usable on its own.
Configuration
A class describing one endpoint: host, port, password, database index, timeouts, serializer. One database means one config class; in detail — Configuration.
The pool
RedisPool keeps one pool per config class. Under Swoole a connection is borrowed on
first use inside a coroutine and returned automatically when it ends; without Swoole
there is one self-maintaining connection per process. Application code never learns the
difference. In detail — Connection pool.
The store
A named slice of the database: a key prefix plus the commands most code actually uses. It is an ordinary object — inject it, use it, test it. In detail — Stores, Hashes, Lists and Streams.
What it looks like end to end
<?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', '');
}
}<?php
namespace Main\Stores;
use Flytachi\Winter\Redis\Store\RedisStore;
use Main\Configurations\MainRedisConfig;
class SessionStore extends RedisStore
{
protected string $redisConfigClassName = MainRedisConfig::class;
protected string $prefix = 'session:';
}use Flytachi\Winter\DI\Attribute\Autowired;
use Main\Stores\SessionStore;
class AuthService
{
#[Autowired]
private SessionStore $sessions;
public function remember(int $userId, string $token): void
{
$this->sessions->set((string) $userId, $token, ttl: 3600);
}
public function forget(int $userId): void
{
$this->sessions->delete((string) $userId);
}
}No connection to obtain, none to return, no select() — the connection is taken on the
first command and goes back to the pool when the request ends.
Installation
composer require flytachi/winter-redisRequires PHP 8.4+ and the ext-redis extension. ext-swoole is optional: with it you
get real pooling, without it one connection with the same liveness checks and lifetime.
Next
- Configuration — config classes, databases, serialization, pool size
- Stores — prefixes, values, lifetimes,
raw() - Hashes — fields and their lifetimes
- Lists — queues, a length cap, blocking reads
- Streams — an event log and consumer groups
- Connection pool — how it works, how to size it, what to do when it fills