Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ephpm/phpredis-shim

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 \Redis is 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's Redis cache 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');

Table of contents


Requirements

  • PHP 8.2+
  • ePHPm (any tagged release) serving the app — the ephpm_kv_* functions have shipped since ePHPm v0.1.0 (and ephpm_kv_flush_all() since v0.1.2). They are provided by the SAPI in both FPM-style and worker (long-lived) modes. Outside ePHPm the SapiKvOps backend throws a RuntimeException; inject Ephpm\Phpredis\InMemoryKvOps in tests.
  • For the global \Redis surface: a PHP build without ext-redis (see the activation rule above). Check with php -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 available

Install

ePHPm 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-shim

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

How it connects (it doesn't)

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.

TTL units

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, ttlseconds.
  • psetex, pexpire, SET … PX, pttlmilliseconds, rounded up to whole seconds before reaching the store (so PX 1500 → a 2 s TTL, never a silently-truncated 1 s). pttl returns true milliseconds.
  • ttl / pttl return -1 for a key with no expiry and -2 for a missing key, exactly like Redis.

Serializer and key prefix

Two phpredis client options are honoured because real apps depend on them:

  • OPT_SERIALIZERSERIALIZER_NONE (default), SERIALIZER_PHP (serialize()/unserialize()), and SERIALIZER_JSON are supported. SERIALIZER_IGBINARY and SERIALIZER_MSGPACK throw NotImplementedException at setOption() time. With SERIALIZER_NONE, non-scalar values are rejected (a RedisException) — set a serializer to store arrays/objects. INCR/DECR always 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.

Coverage matrix

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

Unsupported — throws NotImplementedException

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 _ro variants.
  • 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).
  • SET options XX, KEEPTTL, GET, EXAT, PXAT; conditional EXPIRE modes (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 __call catch-all that throws.

Fidelity limits

Honest list of where the shim differs from real phpredis:

  • setnx / SET … NX cannot distinguish "key exists" from OOM. The ephpm_kv_setnx bool conflates the two; the shim returns false in both cases (as Redis would for key-exists). A plain set/setex failure is unambiguously OOM and throws a RedisException ("OOM …"), matching the RESP wire.
  • type only ever returns REDIS_STRING or REDIS_NOT_FOUND — the store has no other shapes.
  • strlen sees 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/flushDb wipe the entire effective store, not a namespace.
  • Millisecond TTLs round up to whole seconds before the store (the SAPI takes seconds); pttl still reports true milliseconds.

Testing without ePHPm

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

This 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/phpunit

License

MIT — see LICENSE.

About

Userland \Redis (phpredis) shim over ePHPm's in-process ephpm_kv_* bridge; inert when ext-redis is present

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages