YOUR DIGITAL HUB
← Back to blog

Taking over a legacy PHP codebase: diagnosis and mapping across 5 axes

· 10 min read
Cover visual — taking over a legacy PHP codebase, diagnosis and mapping

What we actually see at a legacy client

When we audit a PHP platform that nobody fully understands anymore, the scenery is always the same. A codebase that is ten to fifteen years old, a Zend Framework 1 or a Symfony 2 that was never upgraded, Composer dependencies pinned to abandoned versions, zero automated tests, documentation built out of half-dead Confluence pages and Post-its stuck on monitors. The original developers left a long time ago. The current team ships features while praying not to break anything.

That code produces revenue. Sometimes tens of millions of euros a year. It cannot stop. It cannot be rewritten on a whim either: the big bang kills one client out of two.

Our job is to take over these codebases without breaking the business and move them to a modern foundation in eighteen to thirty-six months. This article describes the method we apply on every takeover engagement.

What "legacy" really means

The popular definition is wrong. Legacy is not "old code". Legacy is code that scares whoever has to change it. That fear has precise causes, and identifying the dominant cause shapes the plan.

  • Functional fear. Nobody knows what the code is supposed to do. Business rules are buried in fifteen-deep nested ifs.
  • Technical fear. The code relies on abandoned patterns (globals, static registries, homegrown ActiveRecord), deprecated APIs, dependencies that nobody supports anymore.
  • Regression fear. No tests, no safety net. Every deploy is a lottery.
  • Operational fear. The build hangs on a 2014 bash script, production runs on a poorly documented physical server, nobody knows how to rebuild the environment.

A Symfony 7.1 codebase written six months ago can be legacy if the original team is gone, tests are missing and business logic is documented nowhere. Conversely, a PHP 5.6 codebase can be perfectly maintainable if it is covered by tests and documented.

Mapping across 5 axes

Before any decision, we map reality on five dimensions. The first four to eight weeks of a takeover are dedicated to this work. Skipping this step is the main mistake internal teams make when they try the takeover on their own.

1. Functional mapping

Understand what the code actually does for the business. Reading source code is not enough: you have to talk to humans and to production data.

  • User interviews. Three to five users per business role. One hour each. Goal: reconstruct the real workflows, identify the critical journeys.
  • Production log analysis. Last 30 days. Which URLs are hit the most? Which cron jobs actually run? Which API endpoints do external systems call?
  • Business workshops. We rebuild the domain in context and container diagrams (C4 levels 1 and 2), data flows. Even when there is older documentation, it is typically wrong.

Deliverable: a C4 context and container diagram, plus an inventory of critical business journeys ranked by business impact.

2. Technical mapping

The full technical inventory. Everything that makes up the system, including what was forgotten long ago.

  • PHP version actually running in prod, installed extensions.
  • Main framework and version. Major libraries (ORM, templating, queue, cache).
  • Composer: composer outdated, composer why, detection of abandoned packages.
  • Database: version, size, tables, stored procedures, triggers.
  • External services: APIs consumed, webhooks, SFTP, shared drives.
  • Cron jobs, workers, queues.
  • Infrastructure: where it runs, how it is deployed, who has access.

Tools to automate that inventory:

# Raw volumetry
phploc src/ --progress

# Complexity metrics
phpmetrics --report-html=metrics src/

# Class and package dependency graph
pdepend --summary-xml=pdepend.xml --dependency-xml=deps.xml src/

# Current coverage (if any tests exist)
phpunit --coverage-text --coverage-html=coverage/

# Dependency security audit
composer audit --format=json > audit.json
symfony security:check

3. Risk mapping

Risks are the list of things that can take the system or the company down. We rank them by probability and impact.

Risk Detection Typical severity
Critical CVE on dependency composer audit + Symfony Security Advisories High
PHP version out of support EOL date on php.net Medium to high
Secrets in source gitleaks detect, targeted grep Critical
Concatenated SQL (injections) PHPStan custom rules, manual review Critical
Deprecated or removed functions Rector PHP8x sets, reverse DowngradePhp80 Medium
unserialize on untrusted input grep + review Critical
XSS in templates Twig/Blade audit, strict mode High
Crons without monitoring Ops mapping Medium
Untested backups IT interview Critical

Our rule: any critical-severity row must have a remediation planned within 30 days, even if the rest of the plan spans eighteen months.

4. Debt mapping

Measure debt with tools, not gut feel. The baseline trio:

  • PHPStan level max. We run level 9 (strict), we count the errors, we generate a baseline. That baseline becomes the debt counter.
  • Rector dry-run. With sets PHP81, SYMFONY_64, CODE_QUALITY, DEAD_CODE. The number of proposed transformations is a good proxy for modernization debt.
  • Deptrac. To measure layer violations. The more violations, the more painful the rewrite will be.

Example of a deptrac.yaml we install in the first week:

parameters:
  paths:
    - src
  exclude_files:
    - '#.*test.*#i'
  layers:
    - name: Controller
      collectors:
        - type: classLike
          value: 'App\\Controller\\.*'
    - name: Service
      collectors:
        - type: classLike
          value: 'App\\Service\\.*'
    - name: Domain
      collectors:
        - type: classLike
          value: 'App\\Domain\\.*'
    - name: Infrastructure
      collectors:
        - type: classLike
          value: 'App\\Infrastructure\\.*'
    - name: Legacy
      collectors:
        - type: classLike
          value: 'Legacy\\.*'
  ruleset:
    Controller:
      - Service
      - Domain
    Service:
      - Domain
    Domain: ~
    Infrastructure:
      - Domain
    Legacy:
      - Controller
      - Service
      - Domain
      - Infrastructure

The Legacy layer is deliberately permissive. It materializes the old zone we wrap, then shrink over the months. Each modernization cycle must reduce the size of that layer, measured in class count and kloc.

5. Human mapping

The human factor kills the most migrations. Who knows what, who is about to leave, who is motivated by the work.

  • Bus factor per module. How many people can maintain each functional block. A bus factor of 1 on a critical module is a red flag.
  • Expected turnover. Departures in progress, long holidays, ending contracts.
  • Tribal documentation. What people know that is written down nowhere. We capture that knowledge in recorded sessions, in Architecture Decision Records, in runbooks.
  • Stance on the project. The sceptics, the movers, the resisters. A major change without a strong internal sponsor fails.

Concrete tools we impose

The first month of a takeover engagement is about installing and running this observation stack, without touching business code.

  • PHPStan with a progressive level and a baseline. Wired into CI within the first week.
  • Psalm as a complement for impossible-code detection and taint analysis.
  • Deptrac for architectural discipline.
  • Rector in dry-run to estimate achievable modernization.
  • PHPMetrics for complexity visualizations, to show to the sponsor and steering committee.
  • phploc for volumetry, compared month over month.
  • PhpDepend for coupling and abstraction graphs.
  • Composer audit and Symfony Security Advisories for CVE hygiene.
  • Gitleaks to detect historical secrets in git, plus rotation of leaked credentials.

Decision matrix: keep, rewrite, wrap, kill

Once the mapping is in hand, every module enters one of four categories. We build this matrix with the business sponsor and the tech lead.

Module Business impact Technical state Decision Rationale
Billing core Critical Messy but working Keep + progressive refactor Too risky to rewrite, high business value
Legacy CSV import Low, monthly Obsolete Kill Replace with a no-code workflow
Internal HR module Medium Very degraded Wrap behind API, rewrite later Isolate to stop touching it
Homegrown full-text search Medium Complex, poorly documented Replace with managed Elasticsearch Immediate ROI
Payment module Critical PCI compliance required Rewrite cleanly Compliance risk
Admin portal Low Barely used Kill Confirmed with business, usage < 5%/month

The rule that ends the most debates: no rewrite without value proof. Rewriting costs 1 to 3 times the initial cost, and during the rewrite the legacy keeps moving. We only authorize a rewrite where wrapping is untenable (compliance, blocking debt, runaway run cost).

Quick wins achievable in one week

Even before the mapping is complete, some gains are immediate and reassure the sponsor. They show that the takeover is active.

  1. composer audit --locked and patching of critical CVEs via targeted upgrades.
  2. Move to the latest supported PHP minor on the current branch (7.4 → 8.0 minimum, ideally 8.3).
  3. Install an error monitoring tool (Sentry or Bugsnag): within 48 hours we discover the silent errors nobody was seeing.
  4. Enable structured logs (Monolog JSON) and ship them to a centralized platform (OpenObserve, Elasticsearch, Loki).
  5. Set display_errors = Off and expose_php = Off in production if it is not the case (still seen in 2025).
  6. Rotate git-leaked secrets.
  7. PHPStan level 1 in CI with baseline, to stop regression.
  8. Test a database backup restore end to end on a test environment.

Recommended 5-step exit plan

After mapping, we propose a structured plan that we adapt to each client.

  1. Stabilize (1 to 3 months). Critical CVEs patched, CI in place, active monitoring, characterization tests on critical journeys. Goal: stop the decay.
  2. Wrap (3 to 6 months). Introduce an anti-corruption layer, expose critical business functions behind clean APIs, tightened Deptrac. Goal: isolate the legacy so that work can happen around it.
  3. Modernize in slices (6 to 18 months). Strangler pattern module by module. Every quarter has a measured target: X% of the Legacy layer rewritten, Y kloc removed, Z endpoints migrated. Never a big bang.
  4. Decommission (12 to 24 months). Delete replaced modules, archive the old, cut dead dependencies. That step is the hardest psychologically: teams hesitate to remove code "just in case".
  5. Consolidate (continuously after 18 months). The platform sits on a modern base, debt is under control, tests cover the critical journeys. We move into standard evolution mode, with strict debt discipline.

Anonymised case: B2B platform, 400 kloc

To give a sense of what this method produces, here is a real anonymised case handled by our team.

  • Context. B2B platform in regulated financial services. 400 kloc PHP. Zend Framework 1.12, Doctrine 1.2, PHP 7.2 in production. Zero automated tests. 3 developers, none present at the 2012 kickoff.
  • Mapping (8 weeks). Inventory of 47 critical business journeys, 12 modules identified, keep/rewrite/wrap/kill matrix validated by the executive committee.
  • Stabilization (4 months). PHP 7.4, Sentry monitoring, 18 critical CVEs patched, tested backup, PHPStan level 2 CI. Zero incidents during that phase.
  • Wrapping (6 months). A new modern REST API exposed through a fresh Symfony 6.4 app, all critical writes routed through it. The legacy front keeps calling the database in read-only mode.
  • Modernization (12 months). Strangler pattern per module: billing, reporting, workflows, admin. At the end, 60% of the Legacy codebase rewritten, PHP 8.3, Symfony 7.1.
  • Results over 18 months. Zero major production regressions. Feature lead time divided by 4. Infrastructure cost reduced by 35% via server consolidation. Team grew from 3 to 7 developers, all trained on the new stack.

Conclusion

Taking over a legacy PHP codebase is neither a leap of faith nor a craft project. It is a method. Mapping on five axes, the decision matrix and the 5-step exit plan let us bring to a controlled risk what many view as an impossible rescue.

For a takeover audit or a long modernization engagement, email us at contact@your-digital-hub.com or discover our PHP expertise and migration services.