theodo-evolution/session-bundle
Symfony bundle from Theodo Evolution that provides session management integration for Symfony apps, with services and configuration for handling user sessions through the framework. It offers a packaged session layer for projects that want session handling wired into Symfony rather than managed manu...
First, calibrate expectations: this is a Symfony2 bundle (PHP 5.3/5.4 era, Symfony 2.1–2.3), not a Laravel package. There is no service provider, no facade, no config/session.php integration. For a Laravel codebase you have two realistic paths:
SessionHandlerInterface / Store. This is the far more likely case; the repo has 46 stars and 0 dependents.DependencyInjection/Configuration.php — the authoritative list of config keys. Don't trust README snippets; this file is the schema.Resources/config/services.yml (and any CompilerPass/) — this tells you how Symfony's session.storage is replaced. That wiring is the actual product.SessionStorageInterface — start(), getId(), getBag(), registerBag(), regenerate(), save(). This is the bridge.Tests/ — cheapest way to learn intended usage without standing up a legacy app.composer require theodo-evolution/session-bundle
// app/AppKernel.php
public function registerBundles() {
return [
// ...
new Theodo\Evolution\SessionBundle\TheodoEvolutionSessionBundle(),
];
}
# app/config/config.yml
framework:
session:
auto_start: false # let the bridge control the lifecycle
theodo_evolution_session:
storage: my_legacy_session_storage # service id, see Configuration.php
# exact key names vary by tag — confirm with:
# app/console config:dump-reference theodo_evolution_session
Point storage at a service that wraps your legacy session (a $_SESSION-backed adapter, or your legacy SessionHandlerInterface). After that, $request->getSession()->get('user') and $_SESSION['user'] read the same data.
The canonical one: the legacy app authenticates and writes $_SESSION['user_id']; a Symfony2 controller needs to read that identity and act on it. Get this working read-only before you allow writes from the Symfony side — reads are safe, writes are where you corrupt sessions.
Laravel analogue to build: a LegacySessionStore extends Illuminate\Session\Store whose read() proxies to the legacy handler, registered via a custom SessionHandlerInterface in config/session.php, plus a StartSession-replacement middleware that boots the legacy session before Laravel's.
The bundle's core move is replacing the container's session.storage service so every Symfony consumer — security, flash bag, CSRF — is unaware of the bridge. Copy that discipline in Laravel: swap the session handler, never sprinkle $_SESSION reads through controllers. If your controllers know about the bridge, you can't remove it later.
Legacy $_SESSION is a flat hash; Symfony uses namespaced bags (_symfony_flashes, _security_<firewall>). The workable pattern is: legacy keys live at the root, Symfony bag contents live under one reserved key (e.g. $_SESSION['_bridge'] = ['flashes' => [...], 'security' => [...]]). Never let Symfony's default bag write to root — it will stomp legacy keys.
In Laravel the equivalents are _flash.new / _flash.old / _token / login_web_<hash>. Put them under a single reserved prefix.
index.php (legacy) boots, starts the session, then includes the Symfony front controller. Symfony must not call session_start() again — configure auto_start: false and have the bridge adopt the already-open session.session_start() a second handler.Pick one per route/URL prefix and be consistent. Mixing them per-request is the #1 source of "my session data vanished" bugs.
Two separate PHP processes only share a session if all of these match:
| Setting | Must match |
|---|---|
session.name |
cookie name |
cookie path / domain / secure |
exactly |
session.save_handler |
files only works on one host; use Redis/DB/memcached for multi-host |
session.save_path |
shared volume or shared server |
session.gc_maxlifetime |
otherwise one app reaps the other's sessions |
If you're on multiple app servers, step one is moving both sides to a shared Redis handler — the bridge won't save you from divergent save_paths.
A good pattern is to make the bridge read-through/write-through but only flush once, at kernel.terminate (Symfony) or TerminateMiddleware (Laravel). That gives you one save point, one regenerate() point, and one place to log diffs. Avoid session_write_close() in the legacy bootstrap before the Symfony side has read — subsequent reads return stale/empty data.
During transition, wrap the storage in a decorator that reads from the legacy store and writes to both the legacy store and a clean Symfony/Laravel store. Once traffic is flipped and the legacy store is cold, drop the decorator. This decorator is also your observability layer — log every key read/written and you'll find the collisions in an afternoon.
$_SESSION in unit tests is a global you'll forget to reset. Fake the underlying handler with an in-memory array and test the storage implementation directly: start → get → set → save → new instance → get. This is the only part of the bundle genuinely worth porting verbatim.
It will not install on a modern stack. Symfony 2.x constraints + PHP 5.x syntax means composer require fails on PHP 8 / Symfony 5+. Vendor the source (it's small) or fork and patch. Do not expect upstream fixes: 46 stars, 0 dependents, no security response.
$_SESSION is a superglobal; copying breaks it. If your storage does $this->data = $_SESSION on start and $_SESSION = $this->data on save, any legacy code holding a reference — or writing between those points — silently diverges. Either hold a reference or make the save unconditional and last.
Bag registration ordering. Symfony throws if you getFlashBag() / getBag() before the bag is registered, and a swapped storage must still register the default bags. If you see The SessionBag ... is not registered, the bridge is starting the session without delegating bag registration. Register bags before start().
Logout and session_destroy() are shared. The legacy app's logout destroys the whole session — including Symfony's security token and flash queue. That's usually desirable, but it means Symfony's logout handler and the legacy logout must not both run, or you'll regenerate an ID twice and log the user out of the wrong context. Centralize logout in one place.
Flash bags are pop-on-read. Symfony's flash bag empties on getFlashBag()->get(). If the legacy side also reads that key, one side sees the message and the other doesn't. Treat flashes as owned by exactly one app; the bridge should not try to be clever here.
Session fixation / trust boundary. Never let a legacy app write an identity key that Symfony's security.context then trusts without re-validation. A legacy session id is not proof of authentication once two apps share the cookie. Re-check the user against your user provider after the bridge reads it.
Config quirks to check explicitly:
framework.session.auto_start: false — non-negotiable in legacy-first mode.session.storage or merely aliases it; run app/console debug:container session.storage and read the class name it resolves to. If it's still NativeSessionStorage, your config key was silently ignored (config:dump-reference first).framework.session.storage_id may need to point at the bridge service, depending on version.Debugging recipe:
var_dump(session_id(), session_name()) at the top of both entry points — they must match before anything else runs.var_dump($_SESSION) immediately after both bootstraps — diff the two arrays.save_path first. Only then move to shared storage — otherwise you're debugging two problems.Extension points worth knowing: a custom AttributeBag if you need to name-map legacy keys without touching either app; a kernel.request listener at high priority if the session must be live before Symfony's own listener; a kernel.terminate listener for the single write-back. These three hooks reimplement cleanly in Laravel as a bootstrap() in app.php, a prepended global middleware, and a terminating middleware respectively.
Bottom line: don't add this to composer.json — read it. The storage decorator, the bag namespacing decision, and the single-write-point pattern are the transferable ideas, and they're 200 lines you can port into a Laravel Store subclass in a day. The Symfony2 wiring itself is dead weight.
How can I help you explore Laravel packages today?