Laravel Cache store backed by
ePHPm's in-process KV store via the ephpm_kv_*
SAPI functions. The same Cache::get/Cache::put/Cache::remember
API your app already uses, zero socket round-trips, zero RESP parsing,
zero serialization beyond what userland code already does.
// php artisan tinker
Cache::store('ephpm')->put('user:42', ['name' => 'Alice'], 3600);
Cache::store('ephpm')->get('user:42'); // ['name' => 'Alice']
Cache::store('ephpm')->increment('hits'); // 1
Cache::store('ephpm')->increment('hits'); // 2Each call resolves to a direct C function call into the Rust DashMap backing ePHPm's KV store. There's no Redis daemon, no Memcached daemon, and there's no socket even in-process; this is the same code path a Rust handler would take.
- Requirements
- Install
- End-to-end: a fresh Laravel project
config/cache.phpsnippet- Setting it as the default cache driver
- Common usage patterns
- Verifying the connection is live
- Supported behavior and limitations
- Testing without ePHPm
- Troubleshooting
- How it works
- License
- PHP 8.2+
- Laravel 10.x, 11.x, or 12.x (
illuminate/contractsandilluminate/supportconstraints are^10.0 || ^11.0 || ^12.0). - The ePHPm runtime, v0.1.2 or newer (current release: v0.10.2).
The
ephpm_kv_*SAPI functions have shipped since ePHPm v0.1.0, butephpm_kv_flush_all()— which backsCache::flush()— arrived in v0.1.2. The 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 store will
refuse to construct.
ePHPm packages are distributed via their GitHub repositories, not
Packagist. Add this repo as a Composer vcs repository, then require
the package (ephpm/cache-laravel is tagged v0.1.0, so ^0.1
resolves):
composer config repositories.ephpm/cache-laravel vcs https://github.com/ephpm/cache-laravel
composer require ephpm/cache-laravel:^0.1Laravel package discovery picks up EphpmCacheServiceProvider
automatically — there is no config/app.php edit required. The
provider calls Cache::extend('ephpm', …) during boot, and the rest
is configuration in config/cache.php.
The shortest possible setup, from composer create-project to a live
cache hit served from in-process memory.
composer create-project laravel/laravel my-app
cd my-app
composer config repositories.ephpm/cache-laravel vcs https://github.com/ephpm/cache-laravel
composer require ephpm/cache-laravel:^0.1'stores' => [
// … other stores …
'ephpm' => [
'driver' => 'ephpm',
'prefix' => 'cache',
],
],In .env:
CACHE_DRIVER=ephpm(Laravel 11+ uses CACHE_STORE — both names point at the same
config; check your config/cache.php for the env() call to be sure.)
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 any Cache::* call now goes
through ePHPm's in-process KV store. No Redis, no Memcached, no
external daemon, no socket round-trip.
Add this entry to the stores array:
'stores' => [
'ephpm' => [
'driver' => 'ephpm',
'prefix' => 'cache',
],
],The prefix is prepended to every key passed to the store — useful
for isolating environments or for the "bump the prefix to flush"
pattern (see Limitations).
Either set the env var:
CACHE_DRIVER=ephpmOr hard-code it in config/cache.php:
'default' => env('CACHE_DRIVER', 'ephpm'),After that, the Cache facade with no store() call resolves to
the ephpm driver:
Cache::put('key', 'value', 60);
Cache::get('key');use Illuminate\Support\Facades\Cache;
Cache::put('user:42:name', 'Alice', now()->addHour());
Cache::get('user:42:name'); // 'Alice'
Cache::forget('user:42:name');$user = Cache::remember('user:42', 60, function () use ($id) {
return User::find($id);
});remember is implemented by Laravel's Repository on top of get +
put, so it works with this store unchanged.
Cache::increment('hits:home'); // 1
Cache::increment('hits:home', 5); // 6
Cache::decrement('hits:home', 2); // 4The counter ops use the SAPI's atomic ephpm_kv_incr_by — no
read-modify-write race even under concurrent requests.
EphpmStore implements Illuminate\Contracts\Cache\LockProvider, so
Laravel's atomic-lock API works — and anything built on it (scheduled
task withoutOverlapping(), Bus::batch() coordination, your own
critical sections):
use Illuminate\Support\Facades\Cache;
$lock = Cache::store('ephpm')->lock('processing:report', 10);
if ($lock->get()) {
// Only one process reaches here at a time.
try {
// … do the exclusive work …
} finally {
$lock->release();
}
}
// Or scope it to a closure — auto-released even on exception:
Cache::lock('processing:report', 10)->get(function () {
// … exclusive work …
});Acquisition is genuinely atomic — it routes through the SAPI's
ephpm_kv_setnx, whose insert-or-fail runs under the KV store's
per-shard lock, so exactly one contended caller wins.
Release is best-effort, not fenced. The ePHPm SAPI currently has no
compare-and-delete / CAS primitive, so — exactly like Laravel's own
MemcachedLock — release() reads the owner token and then deletes in
two separate steps rather than one atomic check-and-delete. There is a
narrow window in which a lock whose TTL just expired and was re-acquired
by another owner could be released by the previous holder. Always set
a TTL (the second argument) so a crashed owner cannot wedge the lock
forever, and do not treat this lock as a hard mutual-exclusion / fencing
token for correctness-critical sections. A fully fenced implementation
is gated on a future CAS SAPI primitive.
// Use ephpm explicitly even if it isn't the default
Cache::store('ephpm')->put('foo', 'bar', 60);
Cache::store('redis')->put('queued', $payload, 30);In .env:
SESSION_DRIVER=cache
CACHE_DRIVER=ephpmLaravel's cache session driver writes through whatever the default
cache store is, so sessions land in ePHPm's KV without any extra
config.
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::attempt(
'send-message:' . $user->id,
5,
fn () => $this->sendMessage(),
60,
);RateLimiter is built on top of the cache, so it works through this
store automatically — including the throttle middleware that protects
your routes.
A two-line health check from php artisan tinker:
Cache::store('ephpm')->put('hello', 'world', 5);
Cache::store('ephpm')->get('hello'); // 'world'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
- The driver is registered and Laravel is routing through it
| Cache method | Backed by ePHPm KV |
|---|---|
Cache::get, Cache::put |
yes |
Cache::remember, Cache::rememberForever |
yes |
Cache::forever |
yes |
Cache::forget |
yes |
Cache::increment, Cache::decrement |
yes (atomic) |
Cache::many, Cache::putMany |
yes (loops get/put) |
Cache::has, Cache::missing |
yes |
Cache::add |
yes (via Repository) |
Cache::lock, Cache::store('ephpm')->lock(...) |
yes (atomic acquire; best-effort release — see below) |
Cache events (CacheHit, KeyWritten, …) |
yes (dispatcher attached) |
Cache::flush / artisan cache:clear |
yes (ePHPm v0.1.2+, via ephpm_kv_flush_all()) |
RateLimiter / throttle middleware |
yes |
Session driver = cache (with this store as default) |
yes |
-
Cache::flush()drops the entire KV store, not just this driver's prefix. It calls the SAPI'sephpm_kv_flush_all()(ePHPm v0.1.2+) — matching how Laravel'sRedisStoreflushes the whole Redis database. Anything else living in the same KV store (sessions, other prefixes) goes with it. On a pre-v0.1.2 runtime the function is absent andflush()returnsfalse; there, bump theprefixinconfig/cache.phpand let old keys age out via TTL. -
No tags.
Cache::tags(['posts', 'comments'])->…will throwBadMethodCallExceptionbecauseEphpmStoredoes not implementIlluminate\Contracts\Cache\TaggableStore. Apps that need tag invalidation should keep a Redis store available for tagged caches and useCache::store('redis')->tags(...)for those call sites. -
TTL minimum is 1 second. Laravel's contract treats
seconds = 0as "expire immediately", which we floor to 1 second to matchRedisStore's behavior. UseCache::forever()for persistent keys. -
In-process state means restart loses cache. This is a design choice — ePHPm's KV is an embedded DashMap, not a durable store. Treat it as a tier-1 cache. See the ePHPm KV docs for the durability/replication story.
EphpmStore 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\Cache\Laravel\EphpmStore;
use Ephpm\Cache\Laravel\InMemoryKvOps;
use Illuminate\Cache\Repository;
$store = new EphpmStore('', new InMemoryKvOps());
$cache = new Repository($store);
$cache->put('foo', 'bar', 60);
assert($cache->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 app
through the ephpm binary (production), or wire EphpmStore directly
with InMemoryKvOps for tests (see above).
Tags aren't supported — see Limitations.
Either drop the tag scoping for that call site or route tagged caches
through a real Redis store (Cache::store('redis')->tags(...)).
Your ePHPm runtime predates v0.1.2, which added the
ephpm_kv_flush_all() SAPI function that backs flush(). Upgrade
ePHPm (current release: v0.10.2); on an old runtime you can bump the
prefix in config/cache.php to force a clean namespace, or rely on
TTLs.
Also by design — the KV store is in-process and not persisted to disk. If you need durability, compose this driver with a write-through to a persistent store, or pick a different cache backend for that specific data.
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_setnx, 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 Laravel
Illuminate\Contracts\Cache\Store and registers it under the ephpm
driver name via the auto-discovered EphpmCacheServiceProvider. The
rest of Laravel's cache stack — Repository, remember, the Cache
facade, the RateLimiter, the throttle middleware, cache-backed
sessions — keeps working unchanged because they all build on the
Store contract.
See ephpm.dev/architecture/kv-store/ for the architecture and ephpm.dev/guides/kv-from-php/ for the underlying SAPI surface.
MIT — see LICENSE.