API Index — Namespaces, Packages, Reports, Indices
Store
in package
Store — backend-agnostic key-value store.
Default backend is OpenSwoole\Table (single-node, in-memory,
lock-free, nanosecond latency — the hot path). Flip to Redis/Valkey
for cross-node + persistent shared state with one line in app.php:
Store::defaultBackend('redis'); // ZEALPHP_REDIS_URL env
Store::defaultBackend('redis', 'redis://cache:6379/0'); // explicit URL
Every existing call site keeps working unchanged — the static API is preserved verbatim.
Column types — prefer the backend-neutral Store::TYPE_* constants
for new code; OpenSwoole\Table::TYPE_* still works for BC:
Store::TYPE_INT — 1, 2, 4, or 8 bytes
Store::TYPE_FLOAT — 8 bytes
Store::TYPE_STRING — up to N bytes (specify max length)
IMPORTANT: Store::make() must be called BEFORE $app->run()
(before workers are forked). The Table backend's shared memory
segment is inherited on fork; the Redis backend's connection pool
is lazily built per worker.
Usage (identical across backends):
Store::make('sessions', 4096, [
'uid' => [Store::TYPE_STRING, 64],
'hits' => [Store::TYPE_INT, 4],
]);
Store::set('sessions', $id, ['uid' => 'alice', 'hits' => 0]);
Store::get('sessions', $id); // ['uid' => 'alice', ...]
Store::incr('sessions', $id, 'hits');
Store::count('sessions'); // O(1)
Table of Contents
Constants
- BACKEND_MEMCACHED : mixed = 'memcached'
- BACKEND_REDIS : mixed = 'redis'
- BACKEND_TABLE : mixed = 'table'
- BACKEND_TIERED : mixed = 'tiered'
- PREFER_AUTO : mixed = 'auto'
- PREFER_PHPREDIS : mixed = 'phpredis'
- PREFER_PREDIS : mixed = 'predis'
- TYPE_FLOAT : mixed = \OpenSwoole\Table::TYPE_FLOAT
- TYPE_INT : mixed = \OpenSwoole\Table::TYPE_INT
- TYPE_STRING : mixed = \OpenSwoole\Table::TYPE_STRING
Properties
- $backend : StoreBackend|null
-
$backendConfig
: array{kind: string, conn?: string|array
} - $tieredAdvisoryScheduled : bool
- #490 — whether the deferred Tiered-advisory worker-start hook is already
registered. Scheduling must be idempotent:
defaultBackend()can be called more than once (re-configuration, test setup) and each call would otherwise append another hook and duplicate the boot warning.
Methods
- clear() : void
- compareAndSet() : bool
- S-2 — optimistic compare-and-swap on a single Store row+column.
- count() : int
- decr() : int
- defaultBackend() : StoreBackend
- Get or set the process-wide default backend.
- del() : bool
- eval() : mixed
- Run a Lua script atomically on the Redis backend. KEYS are raw /
absolute (un-prefixed). Values are passed as
KEYS/ARGV(never interpolated into the script body). Throws on the Table backend. - evalScript() : mixed
- S-1 — execute a Redis Lua script atomically. Redis executes EVAL server-side as a single atomic operation: no other client can observe an intermediate state, no other command interleaves.
- exists() : bool
- get() : mixed
- Returns the row array when
$fieldis null, the field's scalar value when set, orfalseon miss (legacy BC —Store::gethas always returnedfalsefor missing keys; existing callers narrow viais_array()/is_scalar()and depend on that contract). - getStrict() : mixed
- Strict variant of get() — returns null on miss instead of false.
- hasSetOps() : bool
- True when the active backend supports direct SET primitives (sadd/srem/scard/sscanCursor/sdel). Use as the BC-safe guard before calling Set ops from generic / backend-portable code.
- incr() : int
- iterate() : Generator<string, array<string, scalar>>
-
iteratePaged()
: array{cursor: string, rows: array
>} - Paginated iteration (S-3). Returns one batch + an opaque next-cursor.
- make() : Table|null
- Create a named table. Returns the underlying
OpenSwoole\Tablewhen the backend istable, or null for redis (the raw object has no equivalent there — use the backend-neutral methods). - mget() : array<string, array<string, scalar>|null>
- Bulk read. Missing keys come back as
nullin the result map. - mset() : bool
- names() : array<int, string>
- ping() : bool
- Health check. True on Table backend (always reachable); on Redis, PINGs the pool and returns the result.
- publish() : int
- Fire-and-forget Redis pub/sub.
- publishReliable() : string
- Reliable variant via Redis Streams (XADD). Returns the Redis-generated message ID. Durable when Redis has AOF/RDB; at-least-once delivery via consumer groups (one consumer per worker by default).
- sadd() : int
- scard() : int
- sdel() : bool
- set() : bool
- srem() : int
-
sscanCursor()
: array{cursor: string, members: list
} - Paginated SSCAN. Returns one batch + an opaque next-cursor — same
cursor protocol as
iteratePaged. Cursor '0' starts a fresh scan; a returned cursor of '0' signals end-of-scan. - stats() : array<string, int>
- Per-worker operational stats — pool acquires, timeouts, clients created. Empty array on the Table backend (no stats surface).
- table() : Table|null
- Direct access to the underlying
OpenSwoole\Table. Only available on the table backend; throwsStoreExceptionon the redis backend (the raw object has no Redis equivalent — use the static methods). - tieredAdvisory() : string|null
- Advisory for a Tiered backend whose cross-node L1 invalidation is OFF.
- tieredBootChecks() : array<int, string>
- Boot-check advisories for the active Tiered backend. Returns the list of
advisory strings (currently 0 or 1) for the process-wide default backend
— empty when the backend is not Tiered, or when Tiered invalidation is
enabled with a secret. Mirrors
App::redisBootChecks()'s return shape so a boot harness can fold these into the same warning stream. - buildBackend() : StoreBackend
- buildTieredBackend() : TieredBackend
- Tiered backend facade: L1=TableBackend (in-process, ns latency) +
L2=RedisBackend (cross-node, source of truth). The L2 build path
reuses the same conn-opts shape as
'redis'so users only need to learn one config dialect. - emitTieredAdvisory() : void
- Emit the Tiered advisory (if any) for the CURRENT default backend, on the
boot-log channel the other Store/App advisories use. Split from the
decision (
tieredAdvisory(), pure) and the timing (scheduleTieredAdvisory()) so each is testable on its own. - memcachedServersFromEnv() : string
- poolOptsFromEnv() : array{prefer?: "auto"|"phpredis"|"predis"}
- Build pool options from environment. ZEALPHP_REDIS_PREFER picks the client lib — 'auto' (default), 'phpredis', or 'predis'. Use 'predis' for pub/sub subscribers in production until the phpredis SUBSCRIBE + HOOK_ALL spike has been benched in your environment.
- redisOrThrow() : RedisBackend
- Resolve the active backend to a RedisBackend (directly, or as the L2 of a Tiered/CircuitBreaker decorator). Throws when the active backend is Table — set ops require cross-node Redis primitives.
- redisUrlFromEnv() : string
- scheduleTieredAdvisory() : void
- Schedule the Tiered advisory for evaluation at a moment when the operator could actually have satisfied it (#490).
Constants
BACKEND_MEMCACHED
public
mixed
BACKEND_MEMCACHED
= 'memcached'
BACKEND_REDIS
public
mixed
BACKEND_REDIS
= 'redis'
BACKEND_TABLE
public
mixed
BACKEND_TABLE
= 'table'
BACKEND_TIERED
public
mixed
BACKEND_TIERED
= 'tiered'
PREFER_AUTO
public
mixed
PREFER_AUTO
= 'auto'
PREFER_PHPREDIS
public
mixed
PREFER_PHPREDIS
= 'phpredis'
PREFER_PREDIS
public
mixed
PREFER_PREDIS
= 'predis'
TYPE_FLOAT
public
mixed
TYPE_FLOAT
= \OpenSwoole\Table::TYPE_FLOAT
TYPE_INT
public
mixed
TYPE_INT
= \OpenSwoole\Table::TYPE_INT
TYPE_STRING
public
mixed
TYPE_STRING
= \OpenSwoole\Table::TYPE_STRING
Properties
$backend
private
static StoreBackend|null
$backend
= null
$backendConfig
private
static array{kind: string, conn?: string|array}
$backendConfig
= ['kind' => 'table']
$tieredAdvisoryScheduled
#490 — whether the deferred Tiered-advisory worker-start hook is already
registered. Scheduling must be idempotent: defaultBackend() can be
called more than once (re-configuration, test setup) and each call would
otherwise append another hook and duplicate the boot warning.
private
static bool
$tieredAdvisoryScheduled
= false
Methods
clear()
public
static clear(string $name) : void
Parameters
- $name : string
compareAndSet()
S-2 — optimistic compare-and-swap on a single Store row+column.
public
static compareAndSet(string $table, string $key, string $field, string $expected, string $new) : bool
Atomic across nodes (Lua-backed); returns true if the swap landed
(current value matched $expected), false otherwise.
// Increment 'hits' by 1 only if it's still at 42:
Store::compareAndSet('counters', 'user:42', 'hits', '42', '43');
Use when the "natural" Counter::compareAndSet shape doesn't fit (e.g., a Store row with mixed-type columns, not a standalone counter). Throws on Table backend.
Parameters
- $table : string
- $key : string
- $field : string
- $expected : string
- $new : string
Return values
boolcount()
public
static count(string $name) : int
Parameters
- $name : string
Return values
intdecr()
public
static decr(string $name, string $key, string $col[, int $by = 1 ]) : int
Parameters
- $name : string
- $key : string
- $col : string
- $by : int = 1
Return values
intdefaultBackend()
Get or set the process-wide default backend.
public
static defaultBackend([StoreBackendKind|string|null $kind = null ][, string|array<string, mixed> $conn = [] ]) : StoreBackend
Prefers StoreBackendKind (type-safe enum) but accepts the bare
string literal forms for BC: 'table' / 'redis'.
Parameters
- $kind : StoreBackendKind|string|null = null
-
enum or bare string; null to read current
- $conn : string|array<string, mixed> = []
-
redis URL string OR ['url'=>, 'pool_size'=>, 'prefix'=>, 'prefer'=>]
Return values
StoreBackenddel()
public
static del(string $name, string $key) : bool
Parameters
- $name : string
- $key : string
Return values
booleval()
Run a Lua script atomically on the Redis backend. KEYS are raw /
absolute (un-prefixed). Values are passed as KEYS/ARGV (never
interpolated into the script body). Throws on the Table backend.
public
static eval(string $script[, array<int, string> $keys = [] ][, array<int, string> $args = [] ]) : mixed
Parameters
- $script : string
- $keys : array<int, string> = []
- $args : array<int, string> = []
evalScript()
S-1 — execute a Redis Lua script atomically. Redis executes EVAL server-side as a single atomic operation: no other client can observe an intermediate state, no other command interleaves.
public
static evalScript(string $script[, array<int, string> $keys = [] ][, array<int, string> $args = [] ]) : mixed
This is the canonical "transaction" primitive on Redis — every MULTI/EXEC + WATCH pattern has a more efficient Lua equivalent.
Use cases:
-
Atomic "get current value, derive new value, set" without race windows (the CAS pattern, server-side).
-
Multi-key atomic updates (the MULTI/EXEC use case).
-
Conditional ops ("set only if condition holds").
// Atomic compare-and-swap on a hash field: Store::evalScript( "local v = redis.call('HGET', KEYS[1], ARGV[1]); " . "if v == ARGV[2] then return redis.call('HSET', KEYS[1], ARGV[1], ARGV[3]); end; " . "return 0;", ['zealstore:mytable:rowkey'], ['col', 'expected_old', 'new_value'], );
Throws StoreException on Table backend (Lua is Redis-server-side).
MULTI/EXEC + WATCH via the driver protocol is intentionally NOT exposed yet — the Lua approach above covers every documented use case more atomically (one round-trip, server-atomic) and works on both drivers without protocol-level glue. If your workload genuinely needs the deferred-pipeline shape, file an issue with the use case.
Parameters
- $script : string
- $keys : array<int, string> = []
-
Redis keys the script accesses (cluster-routing hint)
- $args : array<int, string> = []
-
ARGV values
exists()
public
static exists(string $name, string $key) : bool
Parameters
- $name : string
- $key : string
Return values
boolget()
Returns the row array when $field is null, the field's scalar
value when set, or false on miss (legacy BC — Store::get has
always returned false for missing keys; existing callers narrow
via is_array() / is_scalar() and depend on that contract).
public
static get(string $name, string $key[, string|null $field = null ]) : mixed
Parameters
- $name : string
- $key : string
- $field : string|null = null
getStrict()
Strict variant of get() — returns null on miss instead of false.
public
static getStrict(string $name, string $key[, string|null $field = null ]) : mixed
Recommended for new code. The legacy get() keeps returning false
on miss for BC with code written before v0.2.39 that uses === false
to detect misses; that BC contract is permanent.
New code that wants the unambiguous null-or-value contract — e.g. to
use ??-style fallbacks safely with stored falsy values — should
call getStrict() instead.
Returns null on miss; the scalar when $field is set and the row exists;
the row array (array<string, scalar>) otherwise.
Parameters
- $name : string
- $key : string
- $field : string|null = null
hasSetOps()
True when the active backend supports direct SET primitives (sadd/srem/scard/sscanCursor/sdel). Use as the BC-safe guard before calling Set ops from generic / backend-portable code.
public
static hasSetOps() : bool
Return values
boolincr()
public
static incr(string $name, string $key, string $col[, int $by = 1 ]) : int
Parameters
- $name : string
- $key : string
- $col : string
- $by : int = 1
Return values
intiterate()
public
static iterate(string $name) : Generator<string, array<string, scalar>>
Parameters
- $name : string
Return values
Generator<string, array<string, scalar>>iteratePaged()
Paginated iteration (S-3). Returns one batch + an opaque next-cursor.
public
static iteratePaged(string $name[, string $cursor = '0' ][, int $count = 100 ]) : array{cursor: string, rows: array>}
Use for large tables where draining the full generator is impractical
(e.g. paginated UI over a 100k-member room roster). When the returned
cursor is '0' the scan is complete.
$next = '0';
do {
$page = Store::iteratePaged('rooms:lobby:members', $next, 100);
foreach ($page['rows'] as $key => $row) { ... }
$next = $page['cursor'];
} while ($next !== '0');
Cursors are NOT stable across schema changes or clear(); resume within a single logically-consistent window. See StoreBackend interface for full contract.
Parameters
- $name : string
- $cursor : string = '0'
- $count : int = 100
Return values
array{cursor: string, rows: arraymake()
Create a named table. Returns the underlying OpenSwoole\Table
when the backend is table, or null for redis (the raw object
has no equivalent there — use the backend-neutral methods).
public
static make(string $name[, int $maxRows = 1024 ][, array<string, array{0: int, 1?: int}> $columns = [] ][, array<string, mixed> $opts = [] ]) : Table|null
Parameters
- $name : string
- $maxRows : int = 1024
- $columns : array<string, array{0: int, 1?: int}> = []
- $opts : array<string, mixed> = []
-
backend-specific: mode/ttl/etc.
Return values
Table|nullmget()
Bulk read. Missing keys come back as null in the result map.
public
static mget(string $name, array<int, string> $keys) : array<string, array<string, scalar>|null>
Parameters
- $name : string
- $keys : array<int, string>
Return values
array<string, array<string, scalar>|null>mset()
public
static mset(string $name, array<string, array<string, scalar>> $rows) : bool
Parameters
- $name : string
- $rows : array<string, array<string, scalar>>
Return values
boolnames()
public
static names() : array<int, string>
Return values
array<int, string>ping()
Health check. True on Table backend (always reachable); on Redis, PINGs the pool and returns the result.
public
static ping() : bool
Return values
boolpublish()
Fire-and-forget Redis pub/sub.
public
static publish(string $channel, string $payload) : int
Scope = the entire cluster. Every app instance connected to
the same Redis (every ZealPHP process on every host) that has
called App::subscribe('<this-channel>', ...) receives the
message. That's Redis pub/sub's native PUBLISH semantics —
there's no "local mode" or per-host limiting. If you only want a
specific server to receive the message, route by channel name
(e.g. ws:server:<server-id> — the pattern WSRouter::sendToClient
uses).
Returns the receiver count Redis itself reported — typically
(subscribed workers per instance) × (instances in the cluster).
A return of 0 means no subscriber was listening when the message
was published. Throws StoreException when the default backend
is not Redis (Table has no pub/sub semantics).
Pair with App::subscribe() to register handlers. Messages
published while a subscriber is mid-reconnect are LOST —
use publishReliable() for at-least-once delivery via Streams.
Parameters
- $channel : string
- $payload : string
Return values
intpublishReliable()
Reliable variant via Redis Streams (XADD). Returns the Redis-generated message ID. Durable when Redis has AOF/RDB; at-least-once delivery via consumer groups (one consumer per worker by default).
public
static publishReliable(string $stream, string $payload[, int|null $maxLen = null ]) : string
Pair with App::subscribeReliable() to register a consumer group handler. Throws StoreException when backend is not Redis.
Parameters
- $stream : string
- $payload : string
- $maxLen : int|null = null
Return values
stringsadd()
public
static sadd(string $key, string ...$members) : int
Parameters
- $key : string
- $members : string
Return values
intscard()
public
static scard(string $key) : int
Parameters
- $key : string
Return values
intsdel()
public
static sdel(string $key) : bool
Parameters
- $key : string
Return values
boolset()
public
static set(string $name, string $key, array<string, scalar> $row) : bool
Parameters
- $name : string
- $key : string
- $row : array<string, scalar>
Return values
boolsrem()
public
static srem(string $key, string ...$members) : int
Parameters
- $key : string
- $members : string
Return values
intsscanCursor()
Paginated SSCAN. Returns one batch + an opaque next-cursor — same
cursor protocol as iteratePaged. Cursor '0' starts a fresh scan;
a returned cursor of '0' signals end-of-scan.
public
static sscanCursor(string $key[, string $cursor = '0' ][, int $count = 100 ]) : array{cursor: string, members: list}
Parameters
- $key : string
- $cursor : string = '0'
- $count : int = 100
Return values
array{cursor: string, members: liststats()
Per-worker operational stats — pool acquires, timeouts, clients created. Empty array on the Table backend (no stats surface).
public
static stats() : array<string, int>
Return values
array<string, int>table()
Direct access to the underlying OpenSwoole\Table. Only available
on the table backend; throws StoreException on the redis backend
(the raw object has no Redis equivalent — use the static methods).
public
static table(string $name) : Table|null
Parameters
- $name : string
Return values
Table|nulltieredAdvisory()
Advisory for a Tiered backend whose cross-node L1 invalidation is OFF.
public
static tieredAdvisory(StoreBackend $backend) : string|null
Returns the warning string when $backend is Tiered (always Redis-L2)
AND enableInvalidation() has not been called, telling the operator
cross-node L1 coherence is OFF, how to turn it on, and the
enable-relative-to-make boot-order requirement. When invalidation IS
enabled but no HMAC secret is set, the message instead warns that any
Redis writer can forge an evict (the C2 trust-mode gap) — a branch that
was unreachable from the boot path until #490 moved the evaluation past
worker start. Returns null when invalidation is enabled with a secret.
Pure (no side effects) so a unit test can assert the decision; the
boot path emits it via elog (see scheduleTieredAdvisory()). Mirrors
the shape of App::redisBootChecks() / App::opcacheLegacyBootCheck().
Parameters
- $backend : StoreBackend
Return values
string|nulltieredBootChecks()
Boot-check advisories for the active Tiered backend. Returns the list of
advisory strings (currently 0 or 1) for the process-wide default backend
— empty when the backend is not Tiered, or when Tiered invalidation is
enabled with a secret. Mirrors App::redisBootChecks()'s return shape so
a boot harness can fold these into the same warning stream.
public
static tieredBootChecks() : array<int, string>
Return values
array<int, string>buildBackend()
private
static buildBackend(array{kind: string, conn?: string|array} $cfg) : StoreBackend
Parameters
-
$cfg
: array{kind: string, conn?: string|array
}
Return values
StoreBackendbuildTieredBackend()
Tiered backend facade: L1=TableBackend (in-process, ns latency) +
L2=RedisBackend (cross-node, source of truth). The L2 build path
reuses the same conn-opts shape as 'redis' so users only need to
learn one config dialect.
private
static buildTieredBackend(string|array<string, mixed> $conn) : TieredBackend
Recognised opts:
- 'url' / pool_size / prefix / prefer → forwarded to the L2 RedisBackend (same as 'redis')
- 'l1_ttl' (int seconds, default 5) → L1 freshness window
- 'invalidation_secret' (string|null) → cross-node L1 invalidation HMAC secret (defaults to env ZEALPHP_TIERED_INVALIDATION_SECRET)
Parameters
- $conn : string|array<string, mixed>
Return values
TieredBackendemitTieredAdvisory()
Emit the Tiered advisory (if any) for the CURRENT default backend, on the
boot-log channel the other Store/App advisories use. Split from the
decision (tieredAdvisory(), pure) and the timing
(scheduleTieredAdvisory()) so each is testable on its own.
private
static emitTieredAdvisory() : void
memcachedServersFromEnv()
private
static memcachedServersFromEnv() : string
Return values
stringpoolOptsFromEnv()
Build pool options from environment. ZEALPHP_REDIS_PREFER picks the client lib — 'auto' (default), 'phpredis', or 'predis'. Use 'predis' for pub/sub subscribers in production until the phpredis SUBSCRIBE + HOOK_ALL spike has been benched in your environment.
private
static poolOptsFromEnv() : array{prefer?: "auto"|"phpredis"|"predis"}
Return values
array{prefer?: "auto"|"phpredis"|"predis"}redisOrThrow()
Resolve the active backend to a RedisBackend (directly, or as the L2 of a Tiered/CircuitBreaker decorator). Throws when the active backend is Table — set ops require cross-node Redis primitives.
private
static redisOrThrow(string $op) : RedisBackend
Parameters
- $op : string
Return values
RedisBackendredisUrlFromEnv()
private
static redisUrlFromEnv() : string
Return values
stringscheduleTieredAdvisory()
Schedule the Tiered advisory for evaluation at a moment when the operator could actually have satisfied it (#490).
private
static scheduleTieredAdvisory() : void
The advisory asks for enableInvalidation(), which MUST run inside a
coroutine — i.e. from an App::onWorkerStart() hook. That is necessarily
LATER than this build moment: Store::defaultBackend() is called at
app.php file scope, before App::init()/run(), when no scheduler
exists. Evaluating the decision here therefore made it STRUCTURALLY
UNSATISFIABLE — an app doing exactly what the message asks still logged
"invalidation is OFF" on every boot, forever. Worse, because that branch
always won at build time, the SECOND advisory (invalidation ON but
UNAUTHENTICATED — a real evict-forgery/DoS signal) was unreachable.
So defer it: register a worker-start hook that arms a 1 ms timer. The
timer fires once the worker-start callback returns to the event loop —
i.e. after EVERY worker-start hook has run — so the decision observes the
final state regardless of whether the operator registered their
enableInvalidation() hook before or after this backend was built (hooks
run in registration order, and this backend is usually built first).
Gated to worker 0 so a 32-worker server logs the advisory once, not 32
times; task workers (higher ids) are skipped by the same gate.
The hook fires against the CURRENT process-wide default backend, not the
instance built here, so a backend swapped after this call is advised
accurately. Scheduling is idempotent — repeated defaultBackend() calls
(tests, re-configuration) must not pile up hooks or duplicate warnings.
If no server ever runs (plain CLI script, unit test) the hook simply
never fires and nothing is logged; Store::tieredBootChecks() remains
the synchronous seam for callers that want the decision on demand.