Redis

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.

Package flytachi/winter-redisOn top of ext-redis + CPoolSwoole optional

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

main/Configurations/MainRedisConfig.php
<?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', '');
  }
}
main/Stores/SessionStore.php
<?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:';
}
php
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

bash
composer require flytachi/winter-redis

Requires 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