Refactoring Legacy PHP Code: From A to Z
Working with legacy code is an inevitable part of any PHP developer's life. Sooner or later, you will encounter a project written in PHP 5.6 (or even 4), where there is no autoloading, global variables are used, and business logic is mixed with HTML. Refactoring such code is not just "tidying up," but a strategic process that can save a project from technical debt and make it maintainable.
In this article, we will break down how to approach refactoring legacy PHP code systematically, without breaking working functionality, and turn a "swamp" into modern, testable code.
1. Diagnosis and Assessment: Where to Start?
Before changing code, you need to understand what you're dealing with. Start with a project audit.
Step 1. Identify "Pain Points"
- Dependencies: Are outdated libraries used? For example,
mysql_*functions instead of PDO or MySQLi. - Architecture: Is there any structure (MVC)? Or is all the code in one
index.phpfile? - Tests: Do automated tests exist? If not, this is your first priority.
Step 2. Analysis Tools
Use static analyzers to quickly identify problem areas:
- PHPStan / Psalm: Will find type errors and signature mismatches.
- Rector: Automatically updates syntax for a new PHP version.
- PhpMetrics: Will show cyclomatic complexity and code cohesion.
Example of running Rector to upgrade from PHP 7.4 to 8.0:
vendor/bin/rector process src/ --set php802. The "Painless Surgery" Strategy: A Step-by-Step Plan
Refactoring legacy code is not a "big bang." It is a series of small, safe steps. Use the "Strangler Fig" technique — gradually replace old modules with new ones, without rewriting everything at once.
Step 1. Cover the Code with Tests
Without tests, refactoring is playing Russian roulette. Start with characterization tests:
class LegacyCalculatorTest extends TestCase { public function testOldCalculation(): void { // Run the old code and record the result $result = legacy_calculation(10, 5); $this->assertEquals(15, $result); }}This creates a "safety net" before making changes.
Step 2. Isolate "Dirty" Logic
If the code works with global variables ($GLOBALS), move them into function parameters:
// Was (global dependency)function processOrder() { global $db; $result = $db->query("SELECT * FROM orders");}
// Became (dependency injection)function processOrder(PDO $db) { $result = $db->query("SELECT * FROM orders");}Step 3. Replace Deprecated Functions
For example, replacing mysql_* with PDO:
// Old code$link = mysql_connect('localhost', 'user', 'pass');mysql_select_db('test', $link);$result = mysql_query("SELECT * FROM users", $link);
// New code (PDO)$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');$stmt = $pdo->query("SELECT * FROM users");$result = $stmt->fetchAll(PDO::FETCH_ASSOC);3. Typical Legacy PHP Problems and Their Solutions
Problem 1: "Spaghetti Code" (Mixing Logic and Presentation)
Solution: Separate the code into layers. Move business logic into separate classes, and HTML into templates (e.g., using Twig).
// Bad: logic and HTML in one fileif ($user->isAdmin()) { echo "<h1>Hello, admin!</h1>";} else { echo "<h1>Hello, user!</h1>";}
// Good: controller + template//