PHP session save handler that stores $_SESSION data in
ePHPm's in-process KV store via the ephpm_kv_*
SAPI functions. Drop-in replacement for the built-in Files / Memcached
/ Redis session handlers — no daemon, no socket, no /var/lib/php/sessions/
to manage. Works with any PHP code that calls session_start(),
including frameworks that wrap it (WordPress, plain CodeIgniter, custom
PHP) without any framework integration.
require __DIR__ . '/vendor/autoload.php';
session_set_save_handler(new \Ephpm\SessionHandler\KvSessionHandler(), true);
session_start();
$_SESSION['user_id'] = 42; // stored in the KV via ephpm_kv_set
echo $_SESSION['user_id']; // read via ephpm_kv_get on the next requestEach $_SESSION access resolves to a direct C call into the Rust
DashMap backing ePHPm's KV store. There's no Redis daemon, no PHP-FPM
shared filesystem assumption, and no inter-process IPC.
- Why this exists
- Requirements
- Install
- Setup: a fresh PHP project
- Setup: WordPress
- Setup: legacy / no-framework apps via
auto_prepend_file - Configuration
- Verifying the handler is active
- Behavior reference
- Limitations
- Testing without ePHPm
- Troubleshooting
- How it works
- License
The companion packages ephpm/cache-laravel and ephpm/cache-symfony
cover frameworks that own the cache abstraction. But sessions are
broader than that — session_start() is a baked-in PHP feature,
older codebases pre-date framework session abstractions, and even
modern code (WordPress, CodeIgniter, large parts of Drupal core) reaches
for native $_SESSION directly.
This package gives all of them the SAPI fast path without touching
their session code. It's the smallest possible swap: replace the
default Files handler with one line in your bootstrap, and every
$_SESSION write becomes an in-process function call into the Rust
KV store instead of a flock() + fwrite() against
/var/lib/php/sessions/sess_….
- PHP 8.2+ with
ext-session(which is bundled with PHP — you almost certainly already have it). - The ePHPm runtime — the global
ephpm_kv_*SAPI functions are registered by ePHPm's embedded PHP. Outside ePHPm,SapiKvOps::__construct()throws fast so you know immediately that you're not running where the handler can work. For development without ePHPm see Testing without ePHPm.
Confirm the SAPI is present from any PHP file:
var_dump(function_exists('ephpm_kv_get')); // expect bool(true)composer require ephpm/session-handlerThat's it. No framework deps, nothing else to pull in.
Smallest possible end-to-end — drop into an ePHPm document root.
my-app/
├── composer.json
├── vendor/
└── public/
└── index.php
{
"name": "acme/my-app",
"require": {
"php": "^8.2",
"ephpm/session-handler": "^0.1"
},
"autoload": {
"files": ["vendor/autoload.php"]
}
}composer install<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Ephpm\SessionHandler\KvSessionHandler;
// Register before session_start(). The `true` second arg makes PHP
// auto-flush via register_shutdown_function() so unwritten changes
// don't get lost if the script doesn't reach a clean exit.
session_set_save_handler(new KvSessionHandler(), true);
session_start();
$_SESSION['hits'] = ($_SESSION['hits'] ?? 0) + 1;
header('Content-Type: text/plain');
echo "You've visited this page {$_SESSION['hits']} times this session.\n";[server]
listen = "127.0.0.1:8080"
document_root = "./public"ephpm serve --config ephpm.tomlOpen http://127.0.0.1:8080/ in a browser, refresh a few times, watch
the counter increment. Every refresh round-trips the session through a
direct function call into the KV store — no files, no flock.
WordPress doesn't use $_SESSION itself, but a lot of plugins do
(WooCommerce, BuddyPress, contact forms, custom auth integrations).
Wire the handler in via a must-use plugin so it's active before any
plugin code runs.
<?php
/*
* Plugin Name: ephpm Session Storage
* Description: Routes WordPress's PHP sessions through ePHPm's KV store.
*/
require_once ABSPATH . 'vendor/autoload.php';
session_set_save_handler(new \Ephpm\SessionHandler\KvSessionHandler(), true);(mu-plugins/ runs before regular plugins; the handler is in place
before anything calls session_start().)
If your WordPress install doesn't already have a Composer-managed
vendor/ directory, install it at the WP root:
cd /path/to/wordpress
composer init --no-interaction --name=site/wp
composer require ephpm/session-handlerThen point the autoload include in the mu-plugin at that vendor path.
For apps that have no Composer setup or no clear bootstrap entry
point, register the handler globally via auto_prepend_file.
<?php
require_once '/path/to/your/vendor/autoload.php';
session_set_save_handler(new \Ephpm\SessionHandler\KvSessionHandler(), true);[php]
ini_overrides = [
["auto_prepend_file", "/var/www/ephpm-session-bootstrap.php"],
]Now the handler is registered on every request, before any application
code runs, regardless of which entry point gets hit. Good for legacy
codebases with hundreds of .php files at the docroot.
The constructor takes three optional arguments:
new KvSessionHandler(
string $prefix = 'php_session:', // key prefix in the KV store
?int $ttlSeconds = null, // null = read session.gc_maxlifetime
?KvOpsInterface $ops = null, // backend override (tests)
);If you're running multiple sites under one ePHPm with [server.sites_dir]
enabled, give each site its own prefix to keep their session keys
isolated:
$host = $_SERVER['HTTP_HOST'] ?? 'default';
session_set_save_handler(new KvSessionHandler(prefix: "session:{$host}:"), true);ePHPm's KV store also has built-in multi-tenant isolation
([server.sites_dir] gives each vhost its own DashMap), so the prefix
is really just human-readable namespacing on top of that — the KV
store already prevents cross-site reads at the store level.
By default the handler reads session.gc_maxlifetime from php.ini
(typically 1440 seconds / 24 minutes). Override per-app:
session_set_save_handler(new KvSessionHandler(ttlSeconds: 7200), true); // 2hThe TTL is reset on every write, so an actively-used session never times out mid-conversation.
A two-line check from any session-using script:
session_start();
echo "session save handler: " . ini_get('session.save_handler') . "\n"; // 'user'
echo "handler class: " . get_class(session_get_save_handler()) . "\n";
// Ephpm\SessionHandler\KvSessionHandlerIf session.save_handler is anything other than user after you
called session_set_save_handler(), something else has overridden it.
| Operation | What happens |
|---|---|
session_start() (id from cookie) |
validateId() checks the KV via ephpm_kv_exists. If missing, PHP regenerates the id. |
Reading $_SESSION for the first time |
read() fetches the serialized payload via ephpm_kv_get. |
$_SESSION['x'] = ... then script end |
write() serializes and stores via ephpm_kv_set with the configured TTL. |
session.lazy_write = 1 + no payload changes |
updateTimestamp() refreshes TTL via ephpm_kv_expire without rewriting the payload. |
session_destroy() |
destroy() calls ephpm_kv_del. |
session_gc() |
No-op (returns 0) — the KV store handles expiry natively via TTL, no external sweep needed. |
| Session cookie expires / gets cleared client-side | Server-side key persists until its TTL elapses, then the KV store reclaims it lazily on next access. |
PHP's built-in session serialization (session.serialize_handler,
default php) is used unchanged — this handler stores whatever bytes
PHP hands it. You can switch to php_serialize or php_binary and
nothing here cares.
- No cross-process locking on a single session id. PHP's Files
handler uses
flock()so two concurrent requests with the same session cookie serialize at the storage layer. This handler doesn't — with ePHPm's threading model all PHP execution is in one process, and you can opt in to single-flighting per session id at the application level if you need it (modern apps usually callsession_write_close()early to avoid serialization, since it kills concurrent AJAX). Most apps don't actually want session locking — but if yours does, this is a behavior change worth knowing about. - Restart loses session state. ePHPm's KV is in-process; an
ephpm restartclears it. If you need session persistence across restarts, either run a clustered ePHPm setup (gossip-replicated KV survives single-node restarts) or stick with the Files handler. This is a deliberate ePHPm design choice — the value prop is sub-microsecond access, which means in-memory. - Cookie management is unchanged. This handler only changes how
session payloads are stored; cookie name, path, secure flag,
SameSite, lifetime — all still controlled by
session.cookie_*ini settings orsession_set_cookie_params().
The constructor accepts an Ephpm\SessionHandler\KvOpsInterface so
you can run tests on plain php-cli without the ePHPm runtime:
use Ephpm\SessionHandler\InMemoryKvOps;
use Ephpm\SessionHandler\KvSessionHandler;
$handler = new KvSessionHandler('php_session:', 1440, new InMemoryKvOps());
$handler->write('sess-test', 'user|s:5:"alice";');
assert($handler->read('sess-test') === 'user|s:5:"alice";');InMemoryKvOps stores everything in PHP arrays — for tests only, no
eviction, no memory limit.
You're running under stock PHP-FPM, Apache mod_php, or php-cli —
not under ePHPm. The handler can only call into the KV store when
ePHPm is the SAPI. Either run your app via ephpm serve, or pass
InMemoryKvOps as the third constructor arg for local tests.
Cookie issues. Check that session_set_save_handler() is called
before session_start(), and confirm the session cookie is being
sent back by the browser (DevTools → Application → Cookies). Also
make sure session.cookie_lifetime isn't 0 if you need persistence
beyond browser close.
Expected — see Limitations. The KV store is in-process. For persistence across restarts use a clustered ePHPm deploy where gossip-replicated KV survives single-node loss.
There's no per-session-id locking. PHP's Files handler used to provide
this; this handler does not. Either call session_write_close() as
soon as you've extracted what you need (the modern preferred pattern,
allows concurrent AJAX), or keep your conflicting writes idempotent.
You're probably not running session_start() on every request, or
the cookie isn't surviving between requests. Add
var_dump(session_id(), $_SESSION) near the top of your script and
verify the same id comes back across refreshes.
ePHPm runs PHP inside the same OS process as the KV store via the
embed SAPI. The store is a Rust DashMap
plus TTL management. ePHPm registers a small set of host functions
(ephpm_kv_get, ephpm_kv_set, ephpm_kv_del, ephpm_kv_exists,
ephpm_kv_expire, ephpm_kv_ttl, ephpm_kv_pttl, ephpm_kv_incr_by)
into PHP's global function table. Calling one is a direct C call into
Rust — no socket, no protocol parser.
This package wraps those functions in a
SessionHandlerInterface + SessionUpdateTimestampHandlerInterface
SessionIdInterfaceimplementation so PHP's existing session machinery —session_start(),$_SESSION,session_destroy(),session.lazy_write, the gc cycle — all route through the SAPI without any application-level changes.
See ephpm.dev/architecture/kv-store/ for the architecture and ephpm.dev/guides/kv-from-php/ for the underlying SAPI surface.
MIT — see LICENSE.