Refactoring legacy PHP code: how to turn chaos into order
Every PHP developer sooner or later encounters legacy code — an old, poorly documented, and often non-working project. Instead of rewriting everything from scratch (which is risky and expensive), competent refactoring allows you to improve the code without stopping business processes. In this article, we will look at how to safely refactor legacy PHP projects while maintaining their functionality.
1. Assessment and preparation: where to start refactoring legacy code
Before touching the code, you need to understand its current state. Skip this stage — and you risk breaking everything.
- Create a backup — a full dump of the database and project files.
- Conduct an audit — find all entry points, dependencies, and "black boxes". Use
grepto search forinclude,require,eval, andmysql_*functions. - Set up the environment — spin up a local copy of the project (Docker, Vagrant, or XAMPP).
- Add logging —
error_logand monitoring will help track errors after changes.
Example of a simple audit via the console:
# Search for deprecated mysql functionsgrep -rn "mysql_query\\|mysql_connect\\|mysql_fetch" /path/to/legacy/project/Important: if the project has no tests — refactoring turns into a game of Russian roulette. First, cover the critical logic with tests (even if they are manual scenarios written down in a document).
2. Refactoring strategy: small steps for big changes
The best strategy for legacy is gradual improvement. Don't try to rewrite the entire project in one weekend. Use the "boy scout rule" approach: leave the code cleaner than you found it.
2.1. Isolate changes
Each change should be minimal and atomic. Example: renaming a variable in one file, then extracting duplicate code into a function.
<?php// Before refactoring (legacy)$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];$result = mysql_query($sql);
// After: at least escape it$id = (int)$_GET['id'];$sql = "SELECT * FROM users WHERE id = $id";$result = mysqli_query($link, $sql);?>2.2. Use automation tools
- PHPStan — static analysis, finds errors without running the code.
- Rector — automatic refactoring (PSR-12, replacing deprecated functions).
- PHP CodeSniffer — code style checking.
- PHPUnit — unit testing.
Example of running Rector to replace mysql_* with mysqli_*:
vendor/bin/rector process src/ --set mysql-to-mysqli3. Specific refactoring techniques for PHP code
3.1. Replacing "spaghetti code" with OOP
Legacy is often written as one huge file with HTML and PHP mixed together. Separate the logic into classes and functions.
<?php// Bad: mixed code$user = getUser($_GET['id']);echo "<h1>Hello, " . $user['name'] . "</h1>";if ($_POST['action'] === 'save') { saveUser($_POST);}
// Good: separation of concernsclass UserController { public function show(int $id): void { $user = $this->userRepository->find($id); $this->render('profile', ['user' => $user]); }}?>3.2. Removing "dead" code
Unused functions, comments, commented-out blocks — all of this creates noise. Delete without mercy, but check via git log that the code isn't used somewhere else.
// Search for unused functions (requires PHPStan)vendo