theodo-evolution/legacy-wrapper-bundle
Theodo Evolution's Symfony bundle for wrapping legacy PHP applications and migrating them incrementally. It routes legacy requests through Symfony, bridges sessions, services, and authentication, and lets old and new code coexist so teams can modernize without a risky big-bang rewrite.
The bundle's job is to let a Symfony app invoke a legacy PHP application (or individual legacy scripts) behind a Symfony route, capture its output, and hand it back as a proper Response. That makes it the seam you build a strangler-pattern migration around: new routes live in Symfony, everything not yet migrated falls through to the legacy code.
composer require theodo-evolution/legacy-wrapper-bundle (this is a 2018-era bundle — expect Symfony 2.x/3.x constraints, not Flex/Symfony 4+).app/AppKernel.php:
new TheodoEvolution\LegacyWrapperBundle\TheodoEvolutionLegacyWrapperBundle(),
Then check app/config/config.yml for whatever legacy_wrapper: block the bundle exposes — read the bundle's DependencyInjection/Configuration.php first, it is the authoritative list of options (legacy root path, session sharing, etc.).README.md → Resources/config/services.xml (what service IDs you actually get) → the wrapper/controller class. The service definitions tell you more than the docs about how the legacy include is wired./admin/reports, add a Symfony route for it, and have the controller delegate to the legacy script through the wrapper. Verify the HTML comes back byte-identical to what Apache served before you touch anything else.The mental model to hold: the wrapper is an adapter, not a reimplementation. Symfony owns routing, DI, and the kernel; the legacy app still owns its own bootstrap, DB access, session, and templates.
Progressive migration with a catch-all. Register your new, Symfony-native routes first and a low-priority catch-all route last that forwards to the legacy front controller. New work goes into Symfony controllers; old paths keep hitting legacy untouched. Migrate a page by deleting its fall-through, not by rewriting the wrapper.
Wrap the front controller, not every script. The lowest-friction integration is to let the legacy app's own index.php run inside the wrapper (with its bootstrap intact) rather than including dozens of leaf scripts. You avoid re-creating the legacy router; you only need to translate the incoming path and superglobals correctly.
Adapt the Symfony Request into legacy superglobals. Most legacy apps read $_GET, $_POST, $_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']. Populate these from the Symfony request before the include, in the controller or a small adapter service:
$_GET = $request->query->all();
$_POST = $request->request->all();
$_SERVER['REQUEST_URI'] = $request->getRequestUri();
$_SERVER['REQUEST_METHOD'] = $request->getMethod();
Turn buffered output into a Response. Wrap the include in ob_start() / ob_get_clean() and build a Symfony Response from the buffer, inheriting status and headers where the bundle exposes them. Keep a thin custom controller that returns the wrapper's result so you control the response type (HTML vs. redirect vs. download).
Share the session deliberately. If both apps need login state, decide once whether Symfony owns the session and legacy reads it, or vice versa. A common pattern is to let the legacy app keep managing its session and have Symfony's security layer read the same PHP session — but only if both use identical cookie name, domain, and save path.
Reuse infrastructure where it's safe. Pointing legacy DB config at the same connection parameters as Symfony's Doctrine config is fine; trying to share the live PDO connection is usually not worth the coupling. Keep the two bootstraps separate and let each open its own connection.
Use kernel subrequests for anything nested. If a legacy page emits links back into Symfony-owned pages, or if you want to compose a Symfony fragment inside a legacy layout, do it via HttpKernel::handle() with a subrequest rather than including files across the boundary.
Write a characterization test around each wrapped route before you migrate it. A Symfony functional test that hits the legacy route and snapshots key content gives you a safety net that survives the eventual rewrite.
exit/die in legacy code bypasses everything. The wrapper cannot return a Response if the legacy script terminates the process. Grep the legacy tree for exit, die, header(, and setcookie( early. A register_shutdown_function that flushes the output buffer is a reasonable last resort.headers_sent() or relies on header() having taken effect will behave differently under the wrapper than under Apache. Test redirects specifically; a legacy header('Location: …') inside a buffer must be translated into a Symfony RedirectResponse, not echoed.$_SERVER after the legacy bootstrap runs is too late; the legacy router has already made decisions. Do the adaptation in one place at the top of the wrapped call.session_start() calls, differing cookie paths, or the Symfony session handler writing over the legacy one produce silent, deeply confusing "user randomly logged out" bugs. Verify with session_id() on both sides that you're talking about the same session.class_exists collisions and duplicate requires. Namespace-aware includes or isolated bootstrapping are safer than letting both autoloaders run over the same tree.chdir. Legacy scripts assume the working directory is the legacy root. If includes fail with "file not found", it is almost always getcwd(). Wrap the include with chdir($legacyRoot) and restore afterwards.var/log instead of a blank page. Disable the Symfony profiler for legacy routes — it will intercept and mangle output.include + output buffering — so it's realistic to own it.How can I help you explore Laravel packages today?