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}

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

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']

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). Returns null when invalidation is enabled with a secret — nothing to warn about.

Pure (no side effects) so a unit test can assert the decision; the build path emits it via elog. 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

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
On this page