Skip to content

YourBOX Symfony Migration

ADR-0002 commits YourBOX to a Symfony 8 strangler migration. This page documents the target layout, how the legacy YourBOX/ tree continues to serve URLs through a Symfony fallback bundle, the order in which routes are ported and the operational signals that make a route safe to delete from the legacy tree.

Plan 003 landed the strangler scaffolding on modernization/baseline: the Symfony 8 skeleton lives under app/, the App\Legacy\LegacyBundle is wired into Kernel.php with priority: -1000, and a HealthController serves /health for the smoke target. The migration table at the bottom of this page reflects which downstream routes are still legacy.

yourbox/
├── app/ # Symfony 8 skeleton
│ ├── bin/console
│ ├── composer.json
│ ├── config/
│ │ ├── bundles.php
│ │ ├── packages/
│ │ ├── routes.yaml
│ │ └── services.yaml
│ ├── public/index.php # Front controller for everything
│ ├── src/
│ │ ├── Kernel.php
│ │ ├── Catalog/ # Bounded context (ADR-0006)
│ │ ├── Engagement/
│ │ ├── Identity/
│ │ ├── Moderation/
│ │ └── Legacy/
│ │ ├── LegacyBundle.php
│ │ ├── LegacyController.php
│ │ └── DependencyInjection/
│ ├── templates/
│ └── var/ # Symfony cache, gitignored
├── YourBOX/ # 2020 PHP tree, frozen
│ └── src/
└── infra/

The Symfony front controller at app/public/index.php handles every request. The nginx configuration documented on the Verification Baseline page roots itself at app/public/ and applies the standard Symfony fallback (try_files $uri $uri/ /index.php?$query_string;).

App\Legacy\LegacyBundle registers two pieces:

  • A controller App\Legacy\LegacyController mapped to the catch all route /{path} with priority: -1000 and requirements: ['path' => '.+']. Any URL not claimed by another controller falls through to this one.
  • A configuration parameter legacy.root_dir pointing at the YourBOX/ directory.

The controller resolves the requested path against legacy.root_dir, refuses anything outside it through a realpath check, streams static assets directly through BinaryFileResponse, and includes PHP files inside an isolated static closure to keep their globals from leaking back into the Symfony container.

#[Route('/{path}', name: 'legacy_fallback', requirements: ['path' => '.+'], priority: -1000)]
public function __invoke(Request $request, string $path = ''): Response
{
$candidate = $path === '' ? 'index.php' : $path;
$real = realpath($this->legacyRoot . '/' . $candidate);
if ($real === false || !str_starts_with($real, realpath($this->legacyRoot) . DIRECTORY_SEPARATOR)) {
throw new NotFoundHttpException();
}
if (preg_match('/\.(css|js|png|jpg|jpeg|gif|svg|mp4|webm|ico)$/i', $real)) {
return new BinaryFileResponse($real);
}
if (str_ends_with($real, '.php')) {
ob_start();
(static function (string $file) { require $file; })($real);
return new Response((string) ob_get_clean());
}
throw new NotFoundHttpException();
}

Routes migrate in the order below. Each step lands as an independent merge request that ships and reviews without coupling to the next one.

  1. Health and observability endpoints. /health and /metrics are new Symfony routes that do not exist in the legacy tree.
  2. Secret loading. ADR-0008. The Symfony kernel validates the env contract at boot. db.php and functions.php switch to the Env helper.
  3. Authentication. ADR-0004. The OIDC and form firewalls replace login_registration.php, login.php, registration.php, confirm.php, forget.php, modify_password.php and reset.php.
  4. Video upload and playback. ADR-0005. The Symfony Catalog context rewrites upload_video.php, form_upload_video.php, video_page.php and player.php on top of Flysystem.
  5. Comments and votes. ADR-0006. The Engagement context rewrites comments/views/posts/view.php, like.php and the inline JavaScript that today builds reply markup inside PHP heredoc strings.
  6. Search. ADR-0003. The Catalog context exposes a search endpoint backed by Meilisearch. searching/answer_request.php and the cache/searchings/ HTML cache are removed.
  7. Profile and account management. The Identity context owns modify_profile.php and management_file.php.
  8. Admin panel. The Admin Panel page documents the EasyAdmin layout that replaces accounts/admin/post/index.php, edit.php and delete.php.

The Symfony Migration runbook (strangle-a-legacy-route) describes the per route procedure with the verification gates.

  1. Inventory the legacy route. Read the legacy PHP file, list its inputs (query string, POST fields, cookies, session keys), outputs (rendered HTML, redirects, flash messages), database queries and external side effects.

  2. Pick the bounded context. Each route belongs to one of Catalog, Engagement, Identity or Moderation per ADR-0006. Place new code under app/src/<Context>/.

  3. Write the domain code. Define value objects, the command or query, the handler, and the repository port. Cover invariants with unit tests that boot nothing.

  4. Write the controller. Thin controller in app/src/<Context>/UserInterface/Controller/. Dispatch the command or query through Symfony Messenger. Render a Twig template under app/templates/<context>/.

  5. Add a functional test. WebTestCase exercising the happy path and the security path (CSRF rejected, auth required, role check). The test lives under app/tests/Functional/<Context>/.

  6. Verify in CI. composer test passes including the new tests, hadolint and Trivy stay green, the Plumber controls pass.

  7. Delete the legacy file. Once the Symfony route covers the URL, delete the matching PHP file from YourBOX/. Search the legacy tree for any cross reference (grep -rn 'YourBOX/src/<old path>') and patch the few remaining require_once chains.

  8. Update the migration table on this page with the new status row.

The status table reflects which legacy routes still exist in YourBOX/ and which are fully rewritten in app/. Update the row at the end of each port.

Bounded context Route group Legacy files Status
Platform /health, /metrics (none) Done
Identity OIDC login, profile login_registration.php, login.php, modify_profile.php Planned
Identity Registration, reset registration.php, confirm.php, forget.php, reset.php Planned
Catalog Upload, playback form_upload_video.php, upload_video.php, video_page.php Planned
Catalog Search searching/answer_request.php Planned
Engagement Comments, votes comments/views/posts/view.php, like.php Planned
Moderation Admin accounts/admin/post/index.php, edit.php, delete.php Planned
  • No new file lands under YourBOX/src/. The legacy tree only shrinks.
  • No require_once statement is added in app/src/. The autoloader handles imports.
  • No Symfony import lands under Domain/ or Application/ in any bounded context. PHPStan rules enforce this through a baseline.
  • The fallback controller cannot serve prod.env, prod.env.enc, the composer.lock or any file outside legacy.root_dir. The functional test LegacyControllerTest::testTraversalIsRejected guards this.
  • A legacy file relies on a global initialized by an earlier require_once chain. When the URL is served through the fallback controller, that chain still executes because the included file is the same as in 2020.
  • Two requests in the same worker can collide on $_SESSION['flash'] if Symfony also uses the flash bag. The Symfony session cookie name is changed to yourbox_sess to avoid collisions with the legacy remember cookie until the auth migration completes.
  • Static asset URLs in legacy CSS files reference paths relative to YourBOX/. The Symfony route falls through and serves them, so nothing breaks until the page is rewritten.