Refactoring Legacy PHP Code: Advanced Techniques and Strategies

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Refactoring Legacy PHP Code: Advanced Level



Working with legacy PHP code is an inevitable reality for many developers. Often, such code is a monolithic application without tests, with global dependencies and a mix of business logic and presentation. Simple "by-eye" refactoring can lead to fatal errors. In this article, we will look at advanced refactoring techniques that will allow you to modernize legacy systems safely and effectively.



1. The "Seam" Strategy: Creating Breakpoints



The first step in refactoring legacy code is creating seams. A seam is a place where you can change the behavior of the code without directly editing it. This is especially important when the code is not covered by tests and you cannot run it in isolation.



Let's consider a classic example: direct work with a database inside a method.



<?php// Legacy codeclass UserReport {    public function getActiveUsers() {        $db = new PDO('mysql:host=localhost;dbname=test', 'root', '');        $stmt = $db->query("SELECT * FROM users WHERE active = 1");        return $stmt->fetchAll();    }}?>


Problem: new PDO(...) creates a hard dependency. To test this method, you need a real database.



Solution: Dependency Injection via Constructor.



<?php// Refactoring: creating a seam via DIclass UserReport {    private PDO $pdo;

public function __construct(PDO $pdo) { $this->pdo = $pdo; }

public function getActiveUsers(): array { $stmt = $this->pdo->query("SELECT * FROM users WHERE active = 1"); return $stmt->fetchAll(); }}

// Usage$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');$report = new UserReport($pdo);?>


Now you can replace the PDO with a mock object in tests. This is a simple but effective seam.



2. The "Sprout Method" and "Sprout Class" Pattern



When you cannot change an existing method (for example, due to a lack of tests), use the "Sprout" strategy — create new code alongside the old one, gradually switching calls.



Example: We have a huge method that validates and saves a user.



<?phpclass UserService {    public function saveUser(array $data): bool {        // 100 lines of validation, logging, and saving        $email = $data['email'] ?? '';        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {            throw new InvalidArgumentException('Invalid email');        }        // ... a lot of other code        $db = new PDO(...);        $stmt = $db->prepare("INSERT INTO users ...");        // ...    }}?>


Instead of refactoring the entire method at once, create a new class for validation.



<?phpclass UserValidator {    public function validate(array $data): void {        if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {            throw new InvalidArgumentException('Invalid email');        }        // Add other checks    }}

class UserService { private UserValidator $validator;

public function __construct(UserValidator $validator) { $this->validator = $validator; }

public function saveUser(array $data): bool { $this->validator->validate($data); // The rest of the code remains for now }}?>


Now you can write unit tests for UserValidator without touching the old code. Gradually, you will extract logic from saveUser into new classes.



3. Eliminating Global States and Statics



Legacy code often uses global variables, singletons, and static methods. This makes the code almost impossible to

Blogs

Book Recommendations