Refactoring Legacy PHP Code: From A to Z
\n\nWorking with legacy PHP code is an inevitable reality for many developers. Projects written 5-10 years ago often represent "spaghetti code" with global variables, mixing business logic and presentation, lack of typing, and outdated functions (mysql_*, ereg, etc.). In this article, we will break down a step-by-step approach to refactoring legacy PHP code that allows you to improve the project without stopping its operation.
\n\nWhy is Refactoring Legacy Code Difficult?
\n\nThe main problem with legacy code is the lack of tests. You cannot be sure that changes won't break existing functionality. Additional challenges:
\n\n- \n
- Lack of documentation — the logic of operation is often unclear \n
- Mixing layers — SQL queries inside HTML templates \n
- Global state — variables accessible from any point in the code \n
- Outdated patterns — using register_globals, mysql_* functions \n
- Hard dependencies — direct object creation using
newinside classes \n
Preparation for Refactoring: Creating a "Safety Net"
\n\nBefore changing the code, you need to ensure you can catch errors. Here are the mandatory steps:
\n\n1. Adding Characterization Tests
\n\nCharacterization tests capture the current behavior of the code. They are written after the code is written and allow you to ensure that refactoring hasn't changed the behavior.
\n\n<?php\n// Example of a characterization test for a legacy function\nclass LegacyCalculatorTest extends PHPUnit\\Framework\\TestCase\n{\n public function testCalculateTotal()\n {\n // Capture current behavior\n $result = legacy_calculate_total(100, 0.2);\n $this->assertEquals(120, $result);\n }\n}\n\n\n2. Creating a "Blanket" of Tests
\n\nStart with integration tests that check key user scenarios (registration, order placement, etc.). This will provide basic confidence during refactoring.
\n\nStep-by-Step Strategy for Refactoring Legacy PHP Code
\n\nWe use the "Strangler Fig Pattern" approach — gradually replacing old components with new ones without stopping the system.
\n\nStep 1: Isolate and Encapsulate Global State
\n\nGlobal variables and functions are the main enemy of refactoring. Start by encapsulating them:
\n\n<?php\n// BEFORE: global function and variable\n$db = mysql_connect('localhost', 'user', 'pass');\nfunction get_user($id) {\n global $db;\n $result = mysql_query("SELECT * FROM users WHERE id = $id", $db);\n return mysql_fetch_assoc($result);\n}\n\n// AFTER: class with explicit dependency\nclass UserRepository {\n private PDO $pdo;\n \n public function __construct(PDO $pdo) {\n $this->pdo = $pdo;\n }\n \n public function findById(int $id): ?array {\n $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');\n $stmt->execute([$id]);\n $user = $stmt->fetch();\n return $user ?: null;\n }\n}\n\n\nStep 2: Separate Logic from Presentation
\n\nTypical legacy code contains SQL and PHP logic directly in HTML. Use the "Template Method" pattern for separation:
\n\n<?php\n// BEFORE: spaghetti code in the template\n?>\n<ul>\n<?php\n$result = mysql_query("SELECT * FROM products");\nwhile ($row = mysql_fetch_assoc($result)) {\n echo "<li>" . $row['name'] . " - " . $row['price'] . " rub.</li>";\n}\n?>\n</ul>\n\n// AFTER: extracting logic into a service\nclass ProductService {\n public function __construct(private ProductRepository $repo) {}\n \n public function getProductList(): array {\n return $this->repo->findAll();\n }\n}\n\n// In the template