Userland \Redis (phpredis) compatibility shim over ePHPm's
in-process key-value bridge (the global ephpm_kv_* SAPI functions). Code
written against the phpredis \Redis API talks straight to ePHPm's embedded,
gossip-replicated KV store — no Redis server, no socket, no RESP wire.
It is the KV-side analogue of ephpm/mysqli-shim:
a drop-in for the subset of Redis that a KV/cache store actually models, and
an honest, loud error for everything that needs a real Redis.
The activation rule — read this first. A userland shim cannot override a loaded extension. The global
\Redis(and\RedisException) class is aliased onto this shim only when ext-redis (phpredis) is NOT loaded; the alias is guarded, so under any build that already has phpredis the real extension wins and this package's global surface is inert. The namespaced API (Ephpm\Phpredis\Redis) works regardless — the global\Redisis a thin alias onto it.
Who this is for:
- PHP embed builds without ext-redis — the stock ePHPm SDK does not compile
phpredis in, so this shim lets
\Redis-based code (WordPress object-cache drop-ins, Laravel'sRediscache store, generic PSR caches over\Redis) run on ePHPm's KV store unmodified, with no external Redis to deploy. - Code that wants the KV store behind a
\Redis-shaped API under any build, via the always-available namespaced class.
// Global surface (only when ext-redis is absent):
$r = new Redis();
$r->connect('127.0.0.1', 6379); // host/port accepted & ignored
$r->set('greeting', 'hello', ['ex' => 60]);
$r->get('greeting'); // 'hello'
$r->incrBy('hits', 5); // 5
$r->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$r->set('user.42', ['name' => 'Ada']); // arrays via the PHP serializer
$r->get('user.42'); // ['name' => 'Ada']
// Namespaced surface (works everywhere, even with ext-redis loaded):
$r = new \Ephpm\Phpredis\Redis();
$r->set('k', 'v');- Requirements
- Install
- How it connects (it doesn't)
- TTL units
- Serializer and key prefix
- Coverage matrix
- Unsupported — throws
NotImplementedException - Fidelity limits
- Testing without ePHPm
- License
- PHP 8.2+
- ePHPm (any tagged release) serving the app — the
ephpm_kv_*functions have shipped since ePHPm v0.1.0 (andephpm_kv_flush_all()since v0.1.2). They are provided by the SAPI in both FPM-style and worker (long-lived) modes. Outside ePHPm theSapiKvOpsbackend throws aRuntimeException; injectEphpm\Phpredis\InMemoryKvOpsin tests. - For the global
\Redissurface: a PHP build without ext-redis (see the activation rule above). Check withphp -m | grep redis— or at runtime:
var_dump(extension_loaded('redis')); // false → shim \Redis active
var_dump(function_exists('ephpm_kv_get')); // true → KV bridge availableePHPm packages are distributed via their GitHub repositories, not Packagist.
Add this repo as a Composer vcs repository, then require the package:
composer config repositories.ephpm/phpredis-shim vcs https://github.com/ephpm/phpredis-shim
composer require ephpm/phpredis-shimsrc/compat/redis.php (the guarded global surface) is loaded through composer's
autoload.files on every request; when ext-redis is loaded it returns
immediately without aliasing anything.
There is no connection. connect() / open() / pconnect() / popen()
accept host, port, timeout, and context arguments and ignore them, always
returning true; isConnected() is always true; select(), auth(), and
swapdb() are tolerated no-ops (the KV store is a single, in-process keyspace
reached with no handshake). ping() returns true (or echoes its argument).
Every data command runs directly against ePHPm's KV store via the ephpm_kv_*
natives — the same store the RESP2 listener and the ephpm/cache PSR adapters
use. In a clustered deployment those writes gossip-replicate to other nodes for
free.
At the PHP boundary the ephpm_kv_* bridge takes TTLs in seconds
(ephpm_kv_pttl reports remaining time in milliseconds). The shim matches
phpredis:
setex,expire,SET … EX,ttl— seconds.psetex,pexpire,SET … PX,pttl— milliseconds, rounded up to whole seconds before reaching the store (soPX 1500→ a 2 s TTL, never a silently-truncated 1 s).pttlreturns true milliseconds.ttl/pttlreturn-1for a key with no expiry and-2for a missing key, exactly like Redis.
Two phpredis client options are honoured because real apps depend on them:
OPT_SERIALIZER—SERIALIZER_NONE(default),SERIALIZER_PHP(serialize()/unserialize()), andSERIALIZER_JSONare supported.SERIALIZER_IGBINARYandSERIALIZER_MSGPACKthrowNotImplementedExceptionatsetOption()time. WithSERIALIZER_NONE, non-scalar values are rejected (aRedisException) — set a serializer to store arrays/objects.INCR/DECRalways operate on the raw stored integer and bypass the serializer, matching phpredis.OPT_PREFIX— prepended to every key, exactly as phpredis does.
Any other option (read timeout, scan mode, compression, …) is accepted and
round-trips through getOption(), but has no effect — there is no socket or
background machinery for it to configure.
Legend: impl = implemented over the KV bridge · no-op = accepted, does
nothing, returns success · throws = not implemented, throws
Ephpm\Phpredis\NotImplementedException (a BadMethodCallException).
| Command(s) | Status |
|---|---|
connect / open / pconnect / popen / close / pclose / isConnected |
no-op — connection args accepted and ignored |
select, auth, swapdb, ping, echo |
no-op / echo |
setOption / getOption (OPT_SERIALIZER, OPT_PREFIX honoured; others stored, inert) |
impl |
get (false on miss) |
impl |
set (with int TTL, or ['ex'=>, 'px'=>, 'nx']) |
impl |
setex, psetex, setnx |
impl |
del / unlink, exists (variadic or array of keys) |
impl |
incr, incrBy, decr, decrBy |
impl |
ttl, pttl |
impl |
expire, pexpire, expireAt, pexpireAt (no conditional mode) |
impl |
type (STRING or NOT_FOUND), strlen |
impl |
mget, mset |
impl |
flushAll, flushDb |
impl — both clear the whole effective store |
_serialize, _unserialize, _prefix |
impl |
getLastError / clearLastError |
null / true |
Everything that needs server-side state ePHPm's KV store does not model. Reach
for these and you get a loud BadMethodCallException, never a silent wrong
answer — keep that workload on a real Redis:
- Transactions / pipelines:
multi,exec,discard,watch,unwatch,pipeline. - Pub/sub:
subscribe,psubscribe,publish,pubsub. - Scripting:
eval,evalSha,script, and the_rovariants. - Key iteration / management:
scan,keys,randomKey,rename,renameNx,move,dump,restore,object,sort,dbSize,info. - String ops with no primitive:
append,getSet(not atomic here),incrByFloat(counters are integer-only),msetnx(no atomic multi-key conditional set),persist(no primitive to clear a TTL). SEToptionsXX,KEEPTTL,GET,EXAT,PXAT; conditionalEXPIREmodes (NX/XX/GT/LT).- Every hash / list / set / sorted-set / stream / geo / bitmap / HyperLogLog
/ cluster command (
hSet,lPush,sAdd,zAdd,xAdd,pfAdd,geoAdd, …) — routed through a__callcatch-all that throws.
Honest list of where the shim differs from real phpredis:
setnx/SET … NXcannot distinguish "key exists" from OOM. Theephpm_kv_setnxbool conflates the two; the shim returnsfalsein both cases (as Redis would for key-exists). A plainset/setexfailure is unambiguously OOM and throws aRedisException("OOM …"), matching the RESP wire.typeonly ever returnsREDIS_STRINGorREDIS_NOT_FOUND— the store has no other shapes.strlensees the stored bytes, so under a serializer it is the length of the serialized form (as on a real server).- No key iteration, so no
SCAN/KEYS; caches that rely on tag-based or prefix flush must use explicit key deletion or TTLs.flushAll/flushDbwipe the entire effective store, not a namespace. - Millisecond TTLs round up to whole seconds before the store (the SAPI
takes seconds);
pttlstill reports true milliseconds.
Ephpm\Phpredis\Redis takes an optional KvOpsInterface, and
Ephpm\Phpredis\InMemoryKvOps implements the KV surface in a PHP array (lazy
TTL expiry, an OOM-simulation hook) — no runtime required:
use Ephpm\Phpredis\Redis;
use Ephpm\Phpredis\InMemoryKvOps;
$redis = new Redis(ops: new InMemoryKvOps()); // per-instance
Redis::setDefaultOpsFactory(fn () => new InMemoryKvOps()); // process-wideThis repo's own suite drives the namespaced class against InMemoryKvOps, and
exercises the guarded global \Redis surface end to end in a child php -n
process (see tests/CompatSurfaceTest.php). On CI the real ext-redis is
installed so constant values are asserted against it.
composer install
vendor/bin/phpunitMIT — see LICENSE.