Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ephpm/session-handler

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 request

Each $_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.


Table of contents


Why this exists

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_….


Requirements

  • PHP 8.2+ with ext-session (which is bundled with PHP — you almost certainly already have it).
  • The ePHPm runtime — any tagged release works (the ephpm_kv_* SAPI functions this handler calls have shipped since ePHPm v0.1.0; current release: v0.10.2). The 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)

Install

ePHPm packages are distributed via their GitHub repositories, not Packagist. Add this repo as a Composer vcs repository, then require the package (ephpm/session-handler is tagged v0.1.0, so ^0.1 resolves):

composer config repositories.ephpm/session-handler vcs https://github.com/ephpm/session-handler
composer require ephpm/session-handler:^0.1

That's it. No framework deps, nothing else to pull in.


Setup: a fresh PHP project

Smallest possible end-to-end — drop into an ePHPm document root.

1. Project layout

my-app/
├── composer.json
├── vendor/
└── public/
    └── index.php

2. composer.json

{
    "name": "acme/my-app",
    "repositories": [
        { "type": "vcs", "url": "https://github.com/ephpm/session-handler" }
    ],
    "require": {
        "php": "^8.2",
        "ephpm/session-handler": "^0.1"
    }
}
composer install

3. public/index.php

<?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";

4. ephpm.toml

[server]
listen = "127.0.0.1:8080"
document_root = "./public"

5. Run it

ephpm serve --config ephpm.toml

Open 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.


Setup: WordPress

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.

wp-content/mu-plugins/ephpm-sessions.php

<?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 config repositories.ephpm/session-handler vcs https://github.com/ephpm/session-handler
composer require ephpm/session-handler:^0.1

Then point the autoload include in the mu-plugin at that vendor path.


Setup: legacy / no-framework apps via auto_prepend_file

For apps that have no Composer setup or no clear bootstrap entry point, register the handler globally via auto_prepend_file.

/var/www/ephpm-session-bootstrap.php

<?php
require_once '/path/to/your/vendor/autoload.php';
session_set_save_handler(new \Ephpm\SessionHandler\KvSessionHandler(), true);

ephpm.toml

[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.


Configuration

The constructor's arguments are all optional:

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)
    bool $lockSessions = false,          // opt-in per-session-id locking (see below)
    int $lockTtlSeconds = 30,            // lock lifetime (frees a crashed owner)
    int $lockSpinIntervalUs = 20_000,    // sleep between spin retries (µs)
    int $lockMaxWaitMs = 5_000,          // max spin before proceeding lock-free
);

Multi-tenant: per-host session prefixes

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.

Custom session lifetime

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);  // 2h

The TTL is reset on every write, so an actively-used session never times out mid-conversation.

Opt-in per-session locking

By default this handler is lock-free (see Limitations). If you need Files-handler-style single-flighting — two concurrent requests sharing one session cookie serialised so their $_SESSION writes don't clobber each other — enable it explicitly:

session_set_save_handler(new KvSessionHandler(lockSessions: true), true);

When enabled, read() acquires a per-session lock at <prefix>lock:<id> via the KV store's atomic ephpm_kv_setnx (the lock primitive), spinning up to lockMaxWaitMs for a contended lock, and close() releases it. Tune the lock with lockTtlSeconds (the safety valve that frees a session whose owner crashed before close()), lockSpinIntervalUs, and lockMaxWaitMs.

Two caveats worth knowing:

  • Best-effort unlock. The SAPI has no compare-and-delete, so the release deletes the lock key without verifying the owner token. The lock TTL, not the unlock, is the real backstop against a stuck lock — the same trade the Files handler's advisory flock() makes.
  • Requires a runtime providing ephpm_kv_setnx. Enabling locking on an older runtime that lacks it surfaces a Call to undefined function the first time a session is read. The default (disabled) path never touches setnx.

Most apps are better off calling session_write_close() early to avoid serialization (it lets concurrent AJAX proceed); reach for locking only when you genuinely need the mutual exclusion.


Verifying the handler is active

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\KvSessionHandler

If session.save_handler is anything other than user after you called session_set_save_handler(), something else has overridden it.


Behavior reference

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.


Limitations

  • No cross-process locking on a single session id by default. PHP's Files handler uses flock() so two concurrent requests with the same session cookie serialize at the storage layer. This handler doesn't — unless you opt in with lockSessions: true (see Opt-in per-session locking), which single-flights per session id via the KV store's atomic setnx. Modern apps usually call session_write_close() early to avoid serialization, since it kills concurrent AJAX, so locking stays off by default. Most apps don't actually want session locking — but if yours does, the opt-in gives you Files-handler parity.
  • Restart loses session state. ePHPm's KV is in-process; an ephpm restart clears 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 or session_set_cookie_params().

Testing without ePHPm

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.


Troubleshooting

RuntimeException: ephpm KV SAPI functions are not available

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.

Session works on first request, fails to persist on second

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.

Sessions disappear after ephpm restart

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.

Two browser tabs interleave session writes

There's no per-session-id locking unless you opt in with lockSessions: true (see Opt-in per-session locking). Either enable that, 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.

Counter $_SESSION['hits'] doesn't increment past 1

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.


How it works

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_setnx, 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. (setnx backs the opt-in per-session lock; the rest cover the core session lifecycle.)

This package wraps those functions in a SessionHandlerInterface + SessionUpdateTimestampHandlerInterface

  • SessionIdInterface implementation 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.


License

MIT — see LICENSE.

About

PHP session save handler storing $_SESSION in ePHPm's embedded KV via the ephpm_kv_* SAPI functions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages