Every senior developer has been there: staring at a 15-year-old PHP codebase that’s held together by global variables, copy-pasted database queries, and prayers. The business depends on it, but touching anything feels like defusing a bomb. Today, I want to share a battle-tested approach to modernizing legacy PHP applications without bringing down production.
The Reality of Legacy Code
After two decades in software engineering, I’ve learned that every codebase becomes legacy eventually. The PHP application you’re proud of today will be someone else’s nightmare in ten years. What separates successful teams isn’t avoiding legacy code—it’s having a systematic strategy for dealing with it.
The most dangerous approach? The “big rewrite.” I’ve seen companies spend millions rebuilding systems from scratch, only to ship two years late with half the features. There’s a better way.
The Strangler Fig Pattern
Nature offers us a perfect metaphor. The strangler fig tree grows around an existing tree, gradually taking over until the original tree is no longer needed. We can apply the same principle to code.
Instead of rewriting everything at once, we:
- Identify a small, isolated piece of functionality
- Write new code alongside the old
- Route traffic to the new implementation gradually
- Remove the old code once we’re confident
Practical Example: Extracting a Data Access Layer
Let’s look at a common pattern in legacy PHP: inline SQL queries scattered throughout the codebase.
Before: The Old Way
// In getUserProfile.php
$result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']);
$user = mysql_fetch_assoc($result);
// In processOrder.php
$result = mysql_query("SELECT * FROM users WHERE id = " . $orderId);
$user = mysql_fetch_assoc($result);
// In sendNewsletter.php
$result = mysql_query("SELECT email FROM users WHERE active = 1");
while ($row = mysql_fetch_assoc($result)) {
// send email
}
Notice the problems? SQL injection vulnerabilities, deprecated mysql_* functions, duplicated logic, and tight coupling everywhere.
After: Introducing a Repository
// src/Repository/UserRepository.php
class UserRepository
{
private PDO $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function findById(int $id): ?User
{
$stmt = $this->connection->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? User::fromArray($data) : null;
}
public function findActiveEmails(): array
{
$stmt = $this->connection->query(
'SELECT email FROM users WHERE active = 1'
);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
}
Now we have type safety, parameterized queries, and a single source of truth. But here’s the key: we don’t change every file at once.
The Incremental Migration Strategy
Here’s my step-by-step approach:
- Create the new abstraction – Write your UserRepository without changing existing code.
- Add tests – Write integration tests that verify the new code produces identical results.
- Migrate one caller at a time – Update a single file, deploy, monitor, repeat.
- Remove dead code – Once all callers use the new code, delete the old functions.
PHP 8.4 Features That Help
PHP 8.4 introduced several features that make refactoring easier:
- Property hooks – Simplify getters/setters and validation logic
- Asymmetric visibility – Allow public read with private write
- Chaining methods on new – Cleaner object instantiation patterns
// Property hooks eliminate boilerplate
class Order
{
private array $items = [];
public int $itemCount {
get => count($this->items);
}
public Money $total {
get => array_reduce(
$this->items,
fn($sum, $item) => $sum->add($item->price),
Money::zero()
);
}
}
Common Pitfalls to Avoid
- Don’t refactor without tests – Add characterization tests first.
- Don’t change behavior and structure simultaneously – One PR for refactoring, another for features.
- Don’t aim for perfection – “Good enough” that ships is better than “perfect” that never does.
- Don’t ignore the database – Schema changes often unlock the biggest improvements.
Conclusion
Legacy PHP doesn’t have to be a career-ending assignment. With patience, a systematic approach, and the right tooling, you can modernize even the most intimidating codebases. The key is incremental change: small, safe steps that compound over time.
Your future self (and your team) will thank you for every hour invested in clean, maintainable code. Start small, be consistent, and remember: every modern codebase was once someone’s legacy project.
Next Steps
- Run
php -mto check your current PHP version and extensions - Set up PHPStan or Psalm for static analysis
- Identify one frequently-modified file as your first refactoring target
- Consider using Rector for automated code upgrades