tyrola/redis-session-handler-bundle
Symfony bundle providing a Redis-backed session handler, letting you store PHP sessions in Redis for faster, shared session storage across servers.
Read this first: you do not install this package in a Laravel app. It is a Symfony2 bundle (BirknerAlex/RedisSessionHandlerBundle, forked/published as tyrola/redis-session-handler-bundle), last released 2016-02-24, archived, 1 star, 0 dependents. It registers a Symfony DI service plus a bundle class — none of which Laravel's container, Kernel, or autoloader knows how to boot. Treat it as a reference implementation of a Redis-backed \SessionHandlerInterface, not a dependency.
Your first move:
SESSION_DRIVER=redis; for 95% of cases that is the answer and this bundle is irrelevant.DependencyInjection/ (Configuration.php, services.xml/*.yml) and its handler class. That's the entire package — a few hundred lines that show: connection setup, key prefixing, TTL on write, and gc() behavior. That is what you'd port.Minimal Laravel equivalent (the thing you should actually write):
// config/session.php
'driver' => 'redis',
'connection' => 'session', // dedicated connection, see Gotchas
'store' => 'session',
'lifetime' => 120,
// config/database.php
'redis' => [
'session' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'database' => 1, // isolate from cache/queues
'prefix' => 'app:session:',
],
],
That's the whole "getting started." If you need bespoke behavior (custom key format, cross-app compatibility, locking), jump to ## Implementation Patterns and write a handler — do not pull in a dead Symfony bundle to get it.
Use this unless you have a specific reason not to. It routes through CacheBasedSessionHandler over the redis cache store, so lifetime becomes the Redis TTL. Set prefix and a separate database per app to keep sessions from colliding with cache/queue keys.
The bundle's real content is a session handler. In Laravel, that becomes a custom driver bound in a service provider:
// app/Providers/SessionServiceProvider.php
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Session;
use Illuminate\Session\CacheBasedSessionHandler;
class SessionServiceProvider extends ServiceProvider
{
public function boot(): void
{
Session::extend('redis-tenant', function ($app) {
$minutes = $app['config']['session.lifetime'];
return new CacheBasedSessionHandler(
$app['cache']->store('session'), $minutes
);
});
}
}
Only drop to a raw SessionHandlerInterface implementation when CacheBasedSessionHandler semantics don't fit — e.g. you need SETEX with a key layout another system expects, or you need SessionUpdateTimestampHandlerInterface::updateTimestamp() to avoid rewriting unchanged payloads (the bundle predates that interface and always wrote).
Minimal raw handler shape, mirroring what the bundle does:
class RedisSessionHandler implements \SessionHandlerInterface
{
public function __construct(private \Redis $redis, private string $prefix, private int $ttl) {}
public function open($path, $name): bool { return true; }
public function close(): bool { return true; }
public function read($id): string|false
{
return $this->redis->get($this->prefix . $id) ?: '';
}
public function write($id, $data): bool
{
return $this->redis->setex($this->prefix . $id, $this->ttl, $data);
}
public function destroy($id): bool
{
$this->redis->del($this->prefix . $id);
return true;
}
public function gc($max_lifetime): int|false { return 0; } // TTL does the work
}
Note gc() returning 0 deliberately: Redis expiry is the GC, and Laravel's session GC lottery is wasted I/O against Redis.
If a Symfony2 app uses this bundle and a Laravel app must read the same sessions, the blocker is not Redis — it's payload format. Symfony stores session.serialize_handler output (php, php_binary, php_serialize); Laravel stores its own serialized payload. Sharing requires matching the serializer, the exact key prefix, and the cookie name/domain. In practice: terminate sessions at an edge (nginx/Envoy) or move to a signed JWT/short-lived token instead. Full session-store sharing across two frameworks is a maintenance trap, and the bundle's 2016-era code gives you no help with it.
When decommissioning the Symfony2 service, inventory first: KEYS/SCAN the prefix, count sessions, check TTL distribution, and confirm whether anything else reads those keys. Then drain by setting the app to write to a new prefix before shutting the handler off. The bundle's config exposes host/port/prefix — grab those values from the app's config.yml rather than guessing.
composer require path that works — it's a Symfony Bundle with a Bundle class, DI extension, and Symfony 2.x constraints. Composer may resolve it, but nothing will boot it. Archived + 0 dependents means no fixes incoming.Cache::lock().allkeys-lru will silently eat sessions. If Redis is shared with cache and the eviction policy is allkeys-lru, sessions evict under memory pressure with no error — users just get logged out. Use a dedicated DB with volatile-lru or noeviction, and monitor evicted_keys.database.redis.options.prefix applies globally; config/database.php connection-level prefix applies per connection. Pick one and be explicit — a doubled prefix (app:session:app:session:...) is the usual symptom.session.lifetime and reality. Laravel refreshes the cache TTL on write. If a system reads but doesn't write, the key expires mid-session. Verify with TTL <key> after a read-only request.session()->regenerate() writes a new ID; the old key lingers until TTL. Fine, but it inflates key counts and confuses audits — use migrate(true) semantics deliberately.gc() is not your cleanup mechanism with Redis. Don't set session.gc_probability/lottery to non-zero "to be safe"; it adds latency for no benefit.redis-cli -n 1 --scan --pattern 'app:session:*' | headredis-cli -n 1 TTL 'app:session:<id>' — sanity-check lifetimeredis-cli -n 1 OBJECT ENCODING <key> — confirm you're not accidentally storing huge hashes/ziplistsredis-cli -n 1 MONITOR in staging while reproducing a logout bug; look for DEL/EXPIREphp artisan tinker → Cache::store('session')->get('...') to confirm the store wiringSession::extend() (driver registration), Illuminate\Contracts\Session\SessionHandler, SessionUpdateTimestampHandlerInterface (skip no-op writes), and session.connection/session.store config (cheapest fix for most problems).use the bundle's classes as an API surface. With one star and a nine-year-old last release, any method signature you copy is your responsibility to maintain — copy the idea, write your own handler, keep it under 80 lines.How can I help you explore Laravel packages today?