API Index — Namespaces, Packages, Reports, Indices
MemcachedBackend
in package
implements
StoreBackend
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 companionMemcachedCounterBackend).- Direct
Store::set/get/del/incr/decr/exists/mget/msetcalls.
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
\Memcachedclient instance for direct use. - count() : int
- Not supported — Memcached has no native SCAN.
- decr() : int|float
- Decrement column
$colby$byviaincr()with a negated delta. Not atomic — seeincr(). - del() : bool
- Delete a row from Memcached. Returns
truewhen the key existed and was removed. - exists() : bool
- Return
truewhen a row exists in Memcached for the given key. - get() : mixed
- Retrieve a row or a single field from Memcached. Returns
nullon miss. When$fieldis provided, returns the scalar value of that column ornullif the column is absent. - incr() : int|float
- Read-modify-write increment of column
$colby$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.
$maxRowsis informational only — Memcached is a global pool with server-side LRU eviction; no hard cap is enforced here. Pass$opts['ttl'](int seconds, default0= no expiry) to set a per-table TTL applied on everyset()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
truewhen at least one server responded togetStats(). Returnsfalsewhen 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 viamake()$opts['ttl']) is applied;0means 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_Classand no__wakeup/__destructruns. Returns the decoded array, ornullon a miss (falsefrom 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 anallowed_classeswhitelist — seedecodeRow()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 fromset()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 port11211when 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
$client
private
Memcached
$client
$incrWarned
private
array<string, true>
$incrWarned
= []
Tables a non-atomic-incr advisory was already emitted for (#344; once per table per worker).
$prefix
private
string
$prefix
= 'zealstore'
$schemas
private
array<string, array<string, array{0: int, 1?: int}>>
$schemas
= []
$tableOpts
private
array<string, array{ttl: int}>
$tableOpts
= []
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
Memcachedcount()
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
intdecr()
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|floatdel()
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
boolexists()
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
boolget()
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|floatiterate()
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: arraymake()
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
boolnames()
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
boolprefix()
Return the key prefix used for all Memcached keys (e.g. zealstore).
public
prefix() : string
Return values
stringset()
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
boolassertMade()
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>|nullencodeRow()
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
stringnormalizeRow()
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
stringwarnNonAtomicIncr()
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