Legacy code is not rewritten: it goes under PHPStan and tests, one level at a time, while it keeps shipping

For software houses with a ten-year-old PHP or Laravel system, where nobody dares touch certain folders any more. Technical debt shrinks with numbers that can only improve, not with a month-long freeze.

Do you recognise at least two of these?

  • There is a folder that "works, better not touch it", and every new hire discovers it alone.
  • A type error (null where an object was expected) reaches production once a month.
  • Tests exist, but nobody knows which really pass; CI has been red for weeks and nobody looks any more.
  • You tried PHPStan: 4,000 errors on the first run, and the attempt ended there.
  • A customer asks for NIS2 compliance and the first question is "who touched this file, and when?".

If you said yes at least twice, the problem is not the developers: it is that a safety net that grows on its own is missing.

What does technical debt really cost?

  • One type bug in production a month: half a day to find it, half to fix it, one customer to absorb it. Twelve a year.
  • Every new developer learning "what not to touch" by trial and error: weeks of onboarding that static analysis would cut to days.
  • A red CI ignored is a CI that does not exist: all the time spent writing it is lost.
  • A full rewrite costs the months of rewriting plus the months in which the old system stays without a net.

How do you bring PHPStan to legacy code without stopping?

  1. Low level plus baseline: PHPStan starts at the level that passes and puts existing errors in a baseline file. From then on CI blocks only new errors. In this site's repository: 350,000 lines, level 3, a baseline of 796 errors that can only go down.
  2. Perimeters at max: new modules and the most delicate ones (payments, access, SEO) run at max level with no baseline and 100% coverage. The rest goes up one level a month.
  3. Characterisation tests before touching legacy: they describe what the code does today, right or wrong, so a fix does not change behaviour by accident.
  4. Mutation testing on the perimeter: Infection changes the code on purpose and checks that tests notice; a test that never fails protects nothing.
  5. Larastan for Laravel: it understands Eloquent, relations and facades, and removes the false positives that made PHPStan get abandoned the first time.

What do you find in CI after the first month?

  • PHPStan green on every push, with a baseline that went down during the month and cannot go back up.
  • The tests that matter (the perimeter) at 100% coverage with a minimum mutation score.
  • A written list of the "do not touch" folders, now with characterisation tests: they can be touched.
  • For NIS2 on the software side: who changed what and when, for every file, with CI as witness. The page NIS2 and penetration testing on the code side explains the rest, with the 31 October 2026 deadline.

Who does it, and why you can trust it

Fourteen years of PHP and Laravel in five software houses (fintech, privacy, travel, public administration), the last six as tech lead; JavaScream open source and articles on application security on Red Hot Cyber. The method is the same written in a European proposal scored 12.3 out of 15: TDD with 100% coverage on new modules, PHPStan at max level, mutation testing. I join your repository on Monday, with your pipeline.

How much does it cost?

By the day, at market rates: from 600 to 800 € a day for a senior Laravel developer inside your team, from 700 to 900 € for application security and penetration testing with a report. The first week is by the day and can be stopped: if after five days you do not see better numbers, you keep the report anyway. The full price list and the page senior Laravel developer for software houses.

The next step is small: 30 free minutes with read access to the repository or one day of logs. You get a document with the three points to start from, yours even if you stop there.

Questions about PHPStan and legacy code

How do you reduce technical debt without stopping the team?

PHPStan with a baseline (blocks only new errors), characterisation tests before touching legacy, one more level a month and perimeters at max on delicate modules. Releases continue.

PHPStan gave 4,000 errors: where do we begin?

They go into the baseline and you start at the level that passes. From that day CI blocks new errors; the baseline shrinks a little with every ticket and cannot grow. Larastan removes Laravel false positives.

What is a characterisation test?

A test that describes what the code does today, even when it is wrong. It lets you touch legacy without changing its behaviour by accident; then defects are fixed one at a time, with the test updated.

What is mutation testing for?

Infection modifies the code on purpose and checks that tests notice. A test that never fails protects nothing: the mutation score says how much tests are really worth, not how many lines they touch.

How long until PHPStan is green in CI?

The baseline and a green CI happen in the first week. Raising the level and widening the max perimeter is ongoing work, usually one level a month, without stopping releases.

Does it relate to NIS2?

Yes, on the software side: NIS2 requires knowing who changed what and when, and managing vulnerabilities. A CI with static analysis and tests is the simplest evidence to show, with the measures deadline of 31 October 2026.

How much does a senior for legacy code cost?

By the day, from 600 to 800 euros a day; the first week can be stopped and leaves the report anyway. The initial 30 minutes are free.