Modernising a Legacy PHP Codebase

Jason Reading
VoucherCodes Tech Blog
5 min readMar 30, 2023

As with any codebase, the VoucherCodes main website project has organically grown over time. Changing teams, ever changing requirements — and even full tech stack shifts — leaves us with a patchwork quilt of old-and-new.

We take care to ensure that even the older parts of our codebase are taken care of, and not fall into the “legacy” category. Here’s some of our learnings around this that hopefully apply to your codebase right now.

In this article we’ll tackle:

  • Project structure
  • Code style
  • Code analysis
  • Testing
  • Refactoring code
Photo by Henri L. on Unsplash

Project Structure

As is industry standard now, your code should adhere to the PSR-4 autoloader (https://www.php-fig.org/psr/psr-4/) namespacing and have a consistent folder structure. This structure in place for an autoloader to work removes the need to explicitly include the classes you’re using in a file. Classes such as user_service should be replaced by namespaced classes e.g. \App\User\UserService and the appropriate folder structure src/User/Userservice.php .

If using PHPStorm, there is a built-in inspection “Class path doesn’t match project structure” that will let you know when a file needs changing to adhere to PSR-4

Tidy up your code

We can use tools such as PHP Codesniffer to auto-format your existing PHP code to match a particular code style, such as PSR-12.

Apply these code style changes in isolation — don’t try to mix in behaviour changes. The goal here is to simplify git diffs and make code reviews easier in the future.

If using PHPStorm, you can configure your code style and have the IDE apply this formatting for you as you write.

Adding confidence to your code changes

When working with a fragile codebase — one in which changes can cause cascading issues — it’s difficult to have the confidence to make the many of the changes we need to modernise.

The best way to help instil this confidence is to add some tests and checks to the code we’re modernising, this will hopefully highlight any potential issues as changes are being made.

Static analysis

We gain confidence that our changes are “correct” by using a static analysis tools. Over the last few years, PHPStan has saved our skin on multiple occasions with immediate feedback that our code likely contains bugs.

PHPStan scans your whole codebase and looks for both obvious & tricky bugs. Even in those rarely executed “if” statements that certainly aren’t covered by tests.
https://phpstan.org/

We took the approach of using a baseline with a reasonably high level to ensure that newly written code is up to scratch. We then focused on reducing this baseline until it’s almost gone completely.

If there’s just one thing that you take away from this article it should be this. Use a static analyser! Just having one makes you a better developer.

Refactoring code

There are many reasons to refactor code, the reason that mostly comes up for us is to add extensibility for new functionality.

We often take the approach of refactoring the current code to permit extensibility, whilst keeping the functionality the same — we can then take advantage of that new extensibility by adding functionality on top. Treating these as two independent steps brings confidence in refactoring your code.

Integration tests over unit tests

When refactoring our code, we’re changing the implementation details — and unit tests are all about the implementation details. So let’s choose a different type of test which focuses on the outcomes.

Integration tests validate a larger section of functionality, and are ideal for when we’re looking to change our approach under the hood. Consider examples like moving from a local filesystem to AWS S3, replacing your mailer, adding a new type of offer.

Surprisingly, integration tests can be easy to write for an existing system! You have a system that already works today. Put some data in, see what pops out. Then copy it into a test doing just that.

The skill lies in knowing what to test. We can’t test everything, but comparing real-world usage to your tests will get you most of the way there. When refactoring, we can assert that it just does the same thing for now — the changes can come later once we’re in a better place.

Keeping your changes atomic

It’s always tempting when refactoring code to go for the big bang. Change everything in one go and release it all at once. Huge diffs like this lead to problems if any issues were to arise.

Consider how you’ll fix any issues that arise shortly after release. A rollback? A fix-forward? Now imagine if you’ve just released five big changes instead of one. A rollback doesn’t sound like a decent option.

It’s best to try to make as small, deployable changes as possible. Release early and often to keep your cadence up. Not only will this help with identifying and fixing issues, you get the added benefit of a team morale boost seeing things ship.

Automated refactoring

We migrated our CLI scripts — think PHP crons and daemons — to symfony/console. Doing so manually could be tedious, repetitive, error-prone work. To support this, we used a tool called Rector.

Rector provided us a tooling for automatically mapping our implementation of console ideas to symfony’s implementation of the console command. You’re given direct access to the AST (abstract syntax tree) describing your code, and you can transform this at-will. Identifying common patterns and transforming them programmatically saves on the manual labour, and avoids the issues with using global search-replace text-based manipulation.

There are even publicly available rector templates to do aid with framework usage, such as rectorphp/rector-symfony.

Know when to stop

We know to avoid the big-bang refactor, but there’s an element of pragmatism that needs to be considered when choosing to refactor anything.

There are many questions you should ask yourself:

Does this code need to be refactored now?
Will a quick fix suffice?
What’s the roadmap around this?
Will this code exist in six months? A year? Two? Ten?!

Code that is ugly, but works, is still code that works. Focus on the work that will improve things in the long term over the short term fixes. It’s okay to leave things for later.

Have you heard? We’re hiring at VoucherCodes! Check out our careers page here.

--

--