Predis Connection backend that routes
Redis-shaped calls to ePHPm's in-process KV store via
the ephpm_kv_* SAPI functions. Same Predis API your app already speaks,
zero socket round-trips, zero serialization, zero RESP parsing.
use Predis\Client;
use Ephpm\Predis\KvConnection;
$client = new Client('ephpm:', [
'connections' => ['ephpm' => KvConnection::class],
]);
$client->set('user:42:name', 'Alice');
$client->expire('user:42:name', 3600);
$client->incrby('hits:home', 1);
$client->get('user:42:name'); // 'Alice'Each call resolves to a direct C function call into the Rust DashMap backing ePHPm's KV store. There's no Redis daemon and there's no socket even in-process; this is the same code path PHP would take if it were written in Rust.
- Requirements
- Install
- End-to-end: a fresh PHP project
- Laravel integration
- Symfony integration
- Generic / framework-less use
- Verifying the connection is live
- Supported commands
- SET modifier matrix
- Testing without ePHPm
- Troubleshooting
- How it works
- License
- PHP 8.2+
predis/predis^2.2 (Composer pulls this in automatically)- The ePHPm runtime — the global
ephpm_kv_*SAPI functions are registered by ePHPm's embedded PHP. If you're running your code under PHP-FPM, Apache mod_php, or the stock PHP CLI, those functions don't exist andSapiKvOps::__construct()throws on instantiation. For development without ePHPm running, see Testing without ePHPm.
You can confirm the SAPI is present from any PHP file with:
var_dump(function_exists('ephpm_kv_get')); // expect bool(true)If you get false, you're not running inside ePHPm and the connector
will refuse to construct.
composer require ephpm/predis-connectionThat's it. Composer pulls in predis/predis if you don't already have it.
If you're starting a brand-new project from scratch:
mkdir my-app && cd my-app
composer init --no-interaction --name=acme/my-app --require=php:^8.2
composer require ephpm/predis-connectionThe shortest possible setup — a single index.php you can drop into an
ePHPm document root.
my-app/
├── composer.json
├── vendor/
└── public/
└── index.php
{
"name": "acme/my-app",
"require": {
"php": "^8.2",
"ephpm/predis-connection": "^0.1"
},
"autoload": {
"files": ["vendor/autoload.php"]
}
}composer install<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Ephpm\Predis\KvConnection;
use Predis\Client;
// One-time setup. In a real app this lives in your DI container or a
// service provider — see the framework sections below.
$redis = new Client('ephpm:', [
'connections' => ['ephpm' => KvConnection::class],
]);
// Use it like Predis with a real Redis behind it. No daemon, no socket.
$redis->incrby('hits:home', 1);
$visits = $redis->get('hits:home');
header('Content-Type: text/plain');
echo "This page has been served {$visits} times.\n";Point ePHPm at the docroot:
[server]
listen = "127.0.0.1:8080"
document_root = "./public"ephpm serve --config ephpm.tomlThen curl http://127.0.0.1:8080/ and watch the counter go up. Every
hit is a single in-process function call into the KV store; no network,
no socket, no RESP parsing.
Laravel already supports Predis as its redis.client driver — you just
register this connection class as a custom scheme.
'redis' => [
'client' => env('REDIS_CLIENT', 'predis'),
'options' => [
'connections' => [
'ephpm' => \Ephpm\Predis\KvConnection::class,
],
'parameters' => [
'scheme' => 'ephpm',
],
// Disabling cluster + read/write split keeps Predis from trying
// to negotiate things ePHPm's KV doesn't speak.
'cluster' => false,
],
'default' => [
'scheme' => 'ephpm',
],
'cache' => [
'scheme' => 'ephpm',
],
],Anything in Laravel that goes through Predis. With the config above:
| Laravel feature | Now backed by ePHPm KV |
|---|---|
Cache::store('redis') |
yes |
Redis::get(...), Redis::set(...) |
yes |
Cache::remember('key', 60, fn …) |
yes |
Session driver = redis |
yes |
| Queue throttling / rate limiters | yes |
| Broadcast presence channels (counters) | yes |
What doesn't (yet): queue workers on the redis connection (uses
BLPOP from the lists family), broadcasting via Redis pub/sub
(SUBSCRIBE/PUBLISH), and any explicit list/hash/set ops you've
written. Those will throw CommandNotSupportedException and you'll need
to either keep a real Redis available for those workloads or move them
to a different transport.
php artisan tinker>>> Redis::set('hello', 'world');
=> "OK"
>>> Redis::get('hello');
=> "world"
>>> Redis::incrby('counter', 7);
=> 7Symfony's symfony/cache Redis adapter accepts a Predis client, so the
wiring is just "build a Predis client whose connection scheme is
ephpm, then hand it to the adapter."
services:
Predis\Client:
arguments:
- 'ephpm:'
-
connections:
ephpm: Ephpm\Predis\KvConnection
# Hand the Predis client to Symfony's Redis cache adapter.
Symfony\Component\Cache\Adapter\RedisAdapter:
arguments:
- '@Predis\Client'
- 'app' # namespace prefixframework:
cache:
app: Symfony\Component\Cache\Adapter\RedisAdapterCache::get, Cache::delete, PSR-6 CacheItemPoolInterface consumers,
PSR-16 Cache\SimpleCache\CacheInterface — anything resolved through
Symfony\Component\Cache now talks to the SAPI.
If you're not on Laravel or Symfony, instantiate Predis directly:
use Predis\Client;
use Ephpm\Predis\KvConnection;
$redis = new Client('ephpm:', [
'connections' => ['ephpm' => KvConnection::class],
]);
// Counter
$redis->incrby('orders:placed', 1);
// Cache with TTL
$redis->set('user:42', json_encode($user), 'EX', 3600);
$cached = $redis->get('user:42');
// Pipeline
$results = $redis->pipeline(function ($pipe) {
$pipe->set('a', '1');
$pipe->set('b', '2');
$pipe->get('a');
$pipe->get('b');
});
// $results === ['OK', 'OK', '1', '2']Wrap it in a singleton or pass it through your DI container — it's a
plain Predis client with a custom transport, so anything that takes
a Predis\Client (or Predis\ClientInterface) keeps working.
A two-line health check you can hit from any PHP entry point to confirm the SAPI surface is wired:
$client->set('__healthcheck', '1', 'EX', 5);
assert($client->get('__healthcheck') === '1');If this round-trips successfully you've confirmed:
- ePHPm's SAPI is loaded (
ephpm_kv_*functions exist) - The KV store is up
- TTL parsing works
- Predis is correctly routing through your registered connection class
| Command(s) | Behavior |
|---|---|
GET, SET, SETEX, PSETEX |
Strings with optional EX/PX TTL. PX/PSETEX round up to seconds. |
DEL, UNLINK, EXISTS |
Multi-key, return count. |
INCR, DECR, INCRBY, DECRBY |
Atomic counter ops via the SAPI's ephpm_kv_incr_by. |
EXPIRE, PEXPIRE, TTL, PTTL |
TTL management. PEXPIRE rounds up. |
TYPE |
Returns string if the key exists, none if not. |
PING, ECHO |
Connection liveness, payload echo. |
SELECT, AUTH, QUIT |
Tolerated as no-ops so framework handshakes don't break. |
Everything else — lists, sets, hashes, sorted sets, streams, scripting,
pub/sub, MULTI/EXEC — raises Ephpm\Predis\CommandNotSupportedException
with a clear "ephpm KV does not implement <CMD>" message. ePHPm's KV
store is intentionally a string + counter store; if you need anything
beyond that, point Predis at a real Redis for those calls.
| Modifier | Status |
|---|---|
EX seconds |
supported |
PX milliseconds |
supported (rounded up to seconds) |
NX, XX |
unsupported (no atomic CAS in the SAPI surface) |
GET |
unsupported |
KEEPTTL |
unsupported |
EXAT, PXAT |
unsupported |
The connection takes an optional KvOpsInterface, so you can swap in a
fake backend that runs anywhere — including standard PHPUnit suites on
plain php-cli:
use Ephpm\Predis\InMemoryKvOps;
use Ephpm\Predis\KvConnection;
use Predis\Client;
$client = new Client(new KvConnection(null, new InMemoryKvOps()));
$client->set('foo', 'bar');
assert($client->get('foo') === 'bar');InMemoryKvOps is for tests only — values live in PHP arrays, there is
no eviction policy, no memory limit, and TTL is best-effort lazy
expiry. Don't use it in production.
You're not running under the ePHPm runtime. Either run your code
through the ephpm binary (production), or pass an InMemoryKvOps as
the second arg of KvConnection (tests).
You called a Redis command outside the supported subset (lists/sets/ hashes/streams/scripts/pub-sub). Either change the call to use the string + counter ops your data model can be expressed with, or keep that workload on a real Redis (you can wire two clients in your app and route per-key-pattern).
You hit a Predis feature that requires capabilities our connection deliberately doesn't expose (subscriber loops, MONITOR, replication discovery, etc.). Same fix as above.
Laravel's redis queue driver uses blocking list ops. Either keep a
real Redis around just for queues (config/queue.php can point a
single connection at it) or switch the queue to the database driver.
ePHPm runs PHP inside the same OS process as the KV store via the embed
SAPI. The store itself is a Rust DashMap
plus TTL management. ePHPm registers a small set of host functions
(ephpm_kv_get, ephpm_kv_set, ephpm_kv_incr_by, ephpm_kv_expire,
ephpm_kv_ttl, ephpm_kv_pttl, ephpm_kv_del, ephpm_kv_exists) into
PHP's global function table. Calling one is a direct C function call
into Rust — no socket, no protocol parser, no value serialization beyond
what userland code already does.
This package wraps those functions in a Predis\Connection so existing
Predis-using code (frameworks, libraries, your own) doesn't need to
know anything has changed. Same Predis\Client::get/set/incr/... API,
different transport underneath.
See ephpm.dev/architecture/kv-store/ for the architecture and ephpm.dev/guides/kv-from-php/ for the underlying SAPI surface.
MIT — see LICENSE.