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

MemcachedBackend
in package
implements StoreBackend

FinalYes

Memcached-backed StoreBackend.

Memcached is a flat KV with native atomic increment/decrement and TTL — no hashes, no sets, no pub/sub, no Lua, no SCAN. This backend fits cross-server cache-like workloads where Redis is overkill (or where Memcached is the established infrastructure).

Row layout: each Store row is serialized as a single Memcached value keyed by {prefix}:{table}:{key}. Multi-column rows round-trip through PHP serialize/unserialize.

Security — object-injection defence: rows are stored as a serialize()d STRING (bypassing ext-memcached's built-in PHP serializer, which would run an unrestricted unserialize() on every get() and fire __wakeup/ __destruct of ANY class present in the payload). On read the string is deserialized with ['allowed_classes' => false], so a hostile blob can only round-trip to scalars/arrays — any object becomes __PHP_Incomplete_Class and no magic methods run. This mirrors every other unserialize() site in the codebase (Cache, Session/utils.php).

Constraints surfaced as StoreException:

  • iterate(), count(), clear() — Memcached has no SCAN. Throws with a clear "use Redis for iteration workloads" message.
  • sadd/srem/scard/sscanCursor/sdel — no Set type.
  • publish/publishReliable/Lua — no pub/sub or scripting.

Use cases that DO work end-to-end:

  • Cache (set/get/del/has + per-key TTL).
  • Counter (via the companion MemcachedCounterBackend).
  • Direct Store::set/get/del/incr/decr/exists/mget/mset calls.

Table of Contents

Interfaces

StoreBackend
Behavioural contract every Store backend implements.

Properties

$client  : Memcached
$incrWarned  : array<string, true>
$prefix  : string
$schemas  : array<string, array<string, array{0: int, 1?: int}>>
$tableOpts  : array<string, array{ttl: int}>

Methods

__construct()  : mixed
clear()  : void
Not supported — tracking every written key would be required.
client()  : Memcached
Return the underlying \Memcached client instance for direct use.
count()  : int
Not supported — Memcached has no native SCAN.
decr()  : int|float
Decrement column $col by $by via incr() with a negated delta. Not atomic — see incr().
del()  : bool
Delete a row from Memcached. Returns true when the key existed and was removed.
exists()  : bool
Return true when a row exists in Memcached for the given key.
get()  : mixed
Retrieve a row or a single field from Memcached. Returns null on miss. When $field is provided, returns the scalar value of that column or null if the column is absent.
incr()  : int|float
Read-modify-write increment of column $col by $by.
iterate()  : Generator<string, array<string, scalar>>
Not supported — Memcached has no native SCAN.
iteratePaged()  : array{cursor: string, rows: array>}
Not supported — Memcached has no native SCAN.
make()  : void
Register a named table with its column schema. $maxRows is informational only — Memcached is a global pool with server-side LRU eviction; no hard cap is enforced here. Pass $opts['ttl'] (int seconds, default 0 = no expiry) to set a per-table TTL applied on every set() call.
mget()  : array<string, array<string, scalar>|null>
Bulk read multiple rows in one getMulti() round-trip.
mset()  : bool
Bulk write multiple rows via one setMulti() round-trip.
names()  : array<int, string>
Return the names of all tables registered via make().
ping()  : bool
Ping the Memcached cluster. Returns true when at least one server responded to getStats(). Returns false when all servers are unreachable.
prefix()  : string
Return the key prefix used for all Memcached keys (e.g. zealstore).
set()  : bool
Serialize and store a row in Memcached. The row is stored as a single serialized value at {prefix}:{table}:{key}. The table-level TTL (set via make() $opts['ttl']) is applied; 0 means no expiry.
assertMade()  : void
Assert that a table has been registered via make().
decodeRow()  : array<int|string, mixed>|null
Safely deserialize a value read back from Memcached. Object injection is blocked via ['allowed_classes' => false]: any object in the blob decodes to __PHP_Incomplete_Class and no __wakeup/__destruct runs. Returns the decoded array, or null on a miss (false from Memcached), a non-string value, or a payload that doesn't deserialize to an array.
encodeRow()  : string
Serialize a row to the string we store in Memcached. Stored as a plain serialize() string (NOT handed to ext-memcached's serializer) so reads can deserialize with an allowed_classes whitelist — see decodeRow() and the class docblock's object-injection note.
normalizeRow()  : array<string, scalar>
Coerce a raw deserialized row to array<string, scalar>, dropping any non-scalar values (which can't arise from set() but may appear from external writes or future schema changes).
parseServers()  : array<int, array{0: string, 1: int}>
Parse a comma-separated host[:port] server list into an array of [host, port] pairs. Defaults to port 11211 when omitted.
rowKey()  : string
Build the Memcached key for a row. Keeps composite keys within Memcached's 250-character limit by SHA1-hashing keys longer than 240 chars.
warnNonAtomicIncr()  : void
Emit the non-atomic-incr advisory ONCE per table per worker (#344) — so a developer relying on Store::incr() for rate-limiting / inventory / view counts under load is warned that Memcached drops increments under contention, instead of silently getting drift.

Properties

$incrWarned

private array<string, true> $incrWarned = []

Tables a non-atomic-incr advisory was already emitted for (#344; once per table per worker).

$schemas

private array<string, array<string, array{0: int, 1?: int}>> $schemas = []

Methods

__construct()

public __construct([string $servers = '127.0.0.1:11211' ][, string $prefix = 'zealstore' ]) : mixed
Parameters
$servers : string = '127.0.0.1:11211'

comma-separated host[:port] list (default 11211 port) e.g. "127.0.0.1" or "cache1:11211,cache2:11211"

$prefix : string = 'zealstore'

clear()

Not supported — tracking every written key would be required.

public clear(string $name) : void

Throws StoreException. Use the flush_all Memcached admin command, or set per-table TTLs via make() and let entries expire naturally.

Parameters
$name : string

client()

Return the underlying \Memcached client instance for direct use.

public client() : Memcached
Return values
Memcached

count()

Not supported — Memcached has no native SCAN.

public count(string $name) : int

Throws StoreException. Use the Redis backend for iteration workloads, or maintain your own cardinality counter.

Parameters
$name : string
Return values
int

decr()

Decrement column $col by $by via incr() with a negated delta. Not atomic — see incr().

public decr(string $name, string $key, string $col[, int|float $by = 1 ]) : int|float
Parameters
$name : string
$key : string
$col : string
$by : int|float = 1
Return values
int|float

del()

Delete a row from Memcached. Returns true when the key existed and was removed.

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

exists()

Return true when a row exists in Memcached for the given key.

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

get()

Retrieve a row or a single field from Memcached. Returns null on miss. When $field is provided, returns the scalar value of that column or null if the column is absent.

public get(string $name, string $key[, string|null $field = null ]) : mixed
Parameters
$name : string
$key : string
$field : string|null = null

incr()

Read-modify-write increment of column $col by $by.

public incr(string $name, string $key, string $col[, int|float $by = 1 ]) : int|float

#344 — NOT atomic across concurrent workers. Memcached has no native hash HINCRBY: rows are stored as serialized arrays, so the native Memcached::increment() (plain ASCII-integer keys only) cannot operate on a multi-column row. Under concurrent increments, two callers read the same value and the last set() wins — increments are silently dropped (a +100 burst can land as e.g. +23). A safe atomic path would need Memcached::cas(), but ext-memcached's CAS-token read is not represented in the toolchain's stub, so it can't be wired without losing static- analysis coverage; rather than ship a half-checked CAS loop, this backend is honest about the limitation and emits a one-time runtime advisory (issue #344's documented minimum). For correct atomic counters use the Redis/Tiered backend (HINCRBY/HINCRBYFLOAT) or the standalone Counter facade — both are server-side atomic.

Parameters
$name : string
$key : string
$col : string
$by : int|float = 1
Return values
int|float

iterate()

Not supported — Memcached has no native SCAN.

public iterate(string $name) : Generator<string, array<string, scalar>>

Throws StoreException. Use the Redis backend for iterable Store tables.

Parameters
$name : string
Return values
Generator<string, array<string, scalar>>

iteratePaged()

Not supported — Memcached has no native SCAN.

public iteratePaged(string $name[, string $cursor = '0' ][, int $count = 100 ]) : array{cursor: string, rows: array>}

Throws StoreException. Use the Redis backend for paginated Store iteration.

Parameters
$name : string
$cursor : string = '0'
$count : int = 100
Return values
array{cursor: string, rows: array>}

make()

Register a named table with its column schema. $maxRows is informational only — Memcached is a global pool with server-side LRU eviction; no hard cap is enforced here. Pass $opts['ttl'] (int seconds, default 0 = no expiry) to set a per-table TTL applied on every set() call.

public make(string $name, int $maxRows, array<string|int, mixed> $columns[, array<string|int, mixed> $opts = [] ]) : void
Parameters
$name : string
$maxRows : int
$columns : array<string|int, mixed>
$opts : array<string|int, mixed> = []

backend-specific: mode/ttl/etc.

mget()

Bulk read multiple rows in one getMulti() round-trip.

public mget(string $name, array<string|int, mixed> $keys) : array<string, array<string, scalar>|null>

Missing keys are returned as null in the result map.

Parameters
$name : string
$keys : array<string|int, mixed>
Return values
array<string, array<string, scalar>|null>

mset()

Bulk write multiple rows via one setMulti() round-trip.

public mset(string $name, array<string|int, mixed> $rows) : bool

Atomicity is per-key (not across all keys). Returns true when all writes succeeded.

Parameters
$name : string
$rows : array<string|int, mixed>
Return values
bool

names()

Return the names of all tables registered via make().

public names() : array<int, string>
Return values
array<int, string>

ping()

Ping the Memcached cluster. Returns true when at least one server responded to getStats(). Returns false when all servers are unreachable.

public ping() : bool
Return values
bool

prefix()

Return the key prefix used for all Memcached keys (e.g. zealstore).

public prefix() : string
Return values
string

set()

Serialize and store a row in Memcached. The row is stored as a single serialized value at {prefix}:{table}:{key}. The table-level TTL (set via make() $opts['ttl']) is applied; 0 means no expiry.

public set(string $name, string $key, array<string|int, mixed> $row) : bool
Parameters
$name : string
$key : string
$row : array<string|int, mixed>

Column-name → value map. All declared columns must be present.

Return values
bool

assertMade()

Assert that a table has been registered via make().

private assertMade(string $name) : void

Throws StoreException when the table is unknown, surfacing the "call Store::make() before App::run()" contract violation clearly.

Parameters
$name : string

decodeRow()

Safely deserialize a value read back from Memcached. Object injection is blocked via ['allowed_classes' => false]: any object in the blob decodes to __PHP_Incomplete_Class and no __wakeup/__destruct runs. Returns the decoded array, or null on a miss (false from Memcached), a non-string value, or a payload that doesn't deserialize to an array.

private static decodeRow(mixed $raw) : array<int|string, mixed>|null
Parameters
$raw : mixed
Return values
array<int|string, mixed>|null

encodeRow()

Serialize a row to the string we store in Memcached. Stored as a plain serialize() string (NOT handed to ext-memcached's serializer) so reads can deserialize with an allowed_classes whitelist — see decodeRow() and the class docblock's object-injection note.

private static encodeRow(array<string, scalar> $row) : string
Parameters
$row : array<string, scalar>
Return values
string

normalizeRow()

Coerce a raw deserialized row to array<string, scalar>, dropping any non-scalar values (which can't arise from set() but may appear from external writes or future schema changes).

private normalizeRow(array<int|string, mixed> $row) : array<string, scalar>
Parameters
$row : array<int|string, mixed>
Return values
array<string, scalar>

parseServers()

Parse a comma-separated host[:port] server list into an array of [host, port] pairs. Defaults to port 11211 when omitted.

private static parseServers(string $servers) : array<int, array{0: string, 1: int}>

Falls back to ['127.0.0.1', 11211] when $servers is empty.

Parameters
$servers : string
Return values
array<int, array{0: string, 1: int}>

rowKey()

Build the Memcached key for a row. Keeps composite keys within Memcached's 250-character limit by SHA1-hashing keys longer than 240 chars.

private rowKey(string $table, string $key) : string
Parameters
$table : string
$key : string
Return values
string

warnNonAtomicIncr()

Emit the non-atomic-incr advisory ONCE per table per worker (#344) — so a developer relying on Store::incr() for rate-limiting / inventory / view counts under load is warned that Memcached drops increments under contention, instead of silently getting drift.

private warnNonAtomicIncr(string $name) : void
Parameters
$name : string
On this page