Part 7 of 8 in Strangler Fig migration

The Day We Deleted 12,000 Lines of Legacy Bridge Code

Eight coordinated commits, two hours, 129 files removed. The Strangler Fig bridge had done its job — now it was time to delete it. Here is the checklist we followed, the traps we avoided, and how we knew the bridge was ready to come down.

A stone bridge built from code blocks, half gray and crumbling into a trash bin, half solid blue

After three months of running both frameworks side by side, it was time: every ZF1 module had been ported to Symfony. The catch-all route was still booting ZF1 on every request, but it was booting it to render nothing. No request reached a ZF1 controller anymore.

The bridge had to come down. Here is what that operation looked like.

How we knew the bridge was ready

We did not guess. We had data:

  • Access log analysis. The bridge logged each time it fell through to ZF1. We monitored these logs for two weeks. Zero hits on the catch-all route.
  • Functional test pass. The Codeception test suite ran every route in the application, both Symfony and ZF1 paths. All tests passed through Symfony only.
  • Manual smoke test. A week of manual testing of every critical workflow: employee CRUD, contract lifecycle, payslip generation, leave management. No page rendered through ZF1.

When the catch-all route stopped catching anything, the bridge was ready.

The eight-commit sequence

The bridge removal was not a single git rm command. It was eight coordinated commits over about two hours:

Commit 1: Remove legacy ZF1 tests

The test suite had ZF1-specific tests that bootstrapped the legacy framework. These were no longer needed.

js
0eb78817 chore: remove legacy zf1 tests

Commit 2: Migrate active utility classes

Some utility classes from the legacy library/Custom/ directory were still in use: form helpers, view helpers, and plugin logic that had been migrated to the Utility namespace but whose old files remained. These had to be permanently moved to their modern namespace.

js
bbc8278b refactor(domain): migrate active utility classes from legacy
          Custom to Utility namespace

Commit 3: Remove the bridge (the main event)

129 files, 12,361 lines deleted. Four bridge components gone in one commit:

js
f01c5124 chore(legacy): remove ZF1 bridge code, library and obsolete
          Composer packages

Deleted in this commit:

  • LegacyFallbackController, the Strangler Fig catch-all (142 lines)
  • LegacyBridgeListener, database and tenant bridge (65 lines)
  • LegacyAutoloader, ZF1 class autoloader (46 lines)
  • Entire library/ directory: Custom, ZendX, Menu, DB/MySQL
  • shardj/zf1-future and laminas/laminas-loader Composer packages
  • All ZF1 test bootstrap references

Commit 4: Remove orphaned service

A MaritalService class that had been refactored months ago was still present in the codebase. No code referenced it.

js
4217c427 chore(domain): remove orphaned MaritalService zombie

Commit 5: Fix PHPStan errors

Removing the bridge exposed 49 PHPStan errors: references to deleted classes in type hints, docblocks, and conditional code. These had to be fixed before the build would pass.

js
1256f3f2 fix(static-analysis): resolve 49 PHPStan errors in
          PaginatableRepositoryTrait and MountingType

Commit 6: Rewrite the front controller

public/index.php had carried ZF1 constants and the legacy autoloader registration since day one of the migration. It was now rewritten as a standard Symfony front controller:

js
5ea79463 fix(infra): rewrite public/index.php as standard Symfony 8
          front controller

Before (Strangler Fig mode):

php
define('APPLICATION_PATH', realpath(__DIR__ . '/../application'));
define('APPLICATION_ENV', getenv('APPLICATION_ENV') ?: 'development');

App\Domain\System\Service\LegacyAutoloader::register();

$kernel = new Kernel($env, $env !== 'prod');
$response = $kernel->handle($request);

After (pure Symfony):

php
$kernel = new Kernel($env, $env !== 'prod');
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Commit 7: Update phpstan.neon

PHPStan had exclusions for legacy files and a bootstrap file that loaded ZF1 classes. These were no longer needed:

js
05e33241 chore(config): update phpstan.neon — remove deleted legacy
          exclusions and stubs bootstrap

Commit 8: Remove APPLICATION_ENV

The APPLICATION_ENV constant was a ZF1 artifact. Modern Symfony uses APP_ENV. This was the last ZF1 reference in the codebase:

js
7d953ac4 chore: remove obsolete APPLICATION_ENV (ZF1 legacy)

The order matters

The commits had to be sequenced carefully:

  • Step 2 (utility migration) had to happen before step 3 (bridge removal). Otherwise, active classes would be deleted.
  • Step 5 (PHPStan fixes) had to happen before step 6 (front controller rewrite). Otherwise, the build would fail with 49 errors and the front controller change could not be verified.
  • Step 3 (bridge removal) was the point of no return. All ZF1 code was gone. Steps 4 through 8 were cleanup and hardening.

After commit 3, the application would not deploy if we had missed a dependency. After commit 6, ZF1 could not boot even if we tried. After commit 8, not a single line of ZF1 code remained in the codebase.

What the deletion felt like

Deleting 12,000 lines of code in two hours is strangely satisfying. The bridge components had been carefully built. Each one solved a real problem. But they were temporary by design. The cleanest commit in the repository history is the one that removes code that is no longer needed.


— Delaa