Introduction: Why Refactoring Legacy Code Is Not a Luxury, But a Necessity
Every PHP developer sooner or later encounters legacy code. It could be a project written 10 years ago on PHP 5.3, or an application that "just works" but causes pain with every change. Refactoring such code is not just about "tidying up" the style, but a critically important process that improves the maintainability, performance, and security of the application.
In this article, we will break down practical approaches to refactoring legacy PHP code, using modern language features (PHP 7.4–8.3) and proven patterns. You will learn how to safely make changes without breaking a working system.
1. Assessment and Documentation of the Current State
The first and most important step is to understand what you are dealing with. Do not attempt to refactor blindly.
1.1. Create a Dependency Map
Use tools like PhpMetrics or Deptrac to visualize the connections between classes. This will help identify "god objects" and circular dependencies.
// Example of a typical "god" class in legacy codeclass UserManager { public function save($data) { // 200 lines of code: validation, sending email, logging, database work } public function sendNewsletter() { // Another 150 lines } public function generateReport() { // And another 100 lines }}1.2. Write Characterization Tests
Before changing the code, ensure the current behavior is documented with tests. Use the "Golden Master" approach (snapshot testing):
// Test that captures the current behaviorpublic function testLegacyCalculation(): void{ $calculator = new OldCalculator(); $result = $calculator->compute(10, 20); // If there is no specification, just save the result $this->assertSame(30, $result);}Tip: If the project has no tests at all, start with integration tests for critical business operations. Use PHPUnit with vfsStream for filesystem isolation.
2. Gradual Improvement: From Global Functions to OOP
Much of legacy PHP code consists of global functions and procedural style. Gradual migration to OOP is a key stage of refactoring.
2.1. Encapsulation of Global Functions
Replace global functions with static class methods, then with full-fledged services.
// Before: global functionfunction send_email($to, $subject, $message) { mail($to, $subject, $message);}
// After: service classclass MailService { public function __construct( private MailerInterface $mailer ) {} public function send(string $to, string $subject, string $message): bool { return $this->mailer->send($to, $subject, $message); }}2.2. Eliminating Global Variables
Global variables ($GLOBALS, $_SESSION directly) are a primary source of bugs. Use the Registry pattern or Dependency Injection (DI).
// Bad: direct work with global variablesfunction getCurrentUser() { global $db; $id = $_SESSION['user_id']; return $db->query("SELECT * FROM users WHERE id = $id");}
// Good: explicit dependency injectionclass UserProvider { public function __construct( private PDO $db, private SessionInterface $session ) {} public function getCurrentUser(): ?User { $id = $this->session->get('user_id'); return $this->db->query('SELECT * FROM users WHERE id = ?', [$id]); }}3. Safe Replacement of Deprecated Constructs
PHP evolves, and many old constructs have become unsafe and