Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
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 $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).
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\Table when the backend is table, 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 null in 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; throws StoreException on 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

$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
bool

count()

public static count(string $name) : int
Parameters
$name : string
Return values
int

decr()

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
int

defaultBackend()

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
StoreBackend

del()

public static del(string $name, string $key) : bool
Parameters
$name : string
$key : string
Return values
bool

eval()

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
bool

get()

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
bool

incr()

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
int

iterate()

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: array>}

make()

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|null

mget()

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
bool

names()

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
bool

publish()

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
int

publishReliable()

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
string

sadd()

public static sadd(string $key, string ...$members) : int
Parameters
$key : string
$members : string
Return values
int

scard()

public static scard(string $key) : int
Parameters
$key : string
Return values
int

sdel()

public static sdel(string $key) : bool
Parameters
$key : string
Return values
bool

set()

public static set(string $name, string $key, array<string, scalar> $row) : bool
Parameters
$name : string
$key : string
$row : array<string, scalar>
Return values
bool

srem()

public static srem(string $key, string ...$members) : int
Parameters
$key : string
$members : string
Return values
int

sscanCursor()

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: list}

stats()

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|null

tieredAdvisory()

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|null

tieredBootChecks()

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
StoreBackend

buildTieredBackend()

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
TieredBackend

emitTieredAdvisory()

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
string

poolOptsFromEnv()

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
RedisBackend

redisUrlFromEnv()

private static redisUrlFromEnv() : string
Return values
string

scheduleTieredAdvisory()

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.

On this page