Рефакторинг legacy кода на PHP: от А до Я
Работа с устаревшим (legacy) PHP-кодом — неизбежная реальность для многих разработчиков. Проекты, написанные 5-10 лет назад, часто представляют собой "спагетти-код" с глобальными переменными, смешиванием бизнес-логики и представления, отсутствием типизации и устаревшими функциями (mysql_*, ereg и т.д.). В этой статье мы разберем пошаговый подход к рефакторингу legacy PHP-кода, который позволит улучшить проект без остановки его работы.
Почему рефакторинг legacy кода — это сложно?
Главная проблема legacy кода — отсутствие тестов. Вы не можете быть уверены, что изменения не сломают существующую функциональность. Дополнительные сложности:
- Отсутствие документации — логика работы часто непонятна
- Смешивание слоев — SQL-запросы внутри HTML-шаблонов
- Глобальное состояние — переменные, доступные из любой точки кода
- Устаревшие паттерны — использование register_globals, mysql_* функций
- Жесткие зависимости — прямое создание объектов через
newвнутри классов
Подготовка к рефакторингу: создание "сети безопасности"
Прежде чем менять код, необходимо убедиться, что вы сможете отловить ошибки. Вот обязательные шаги:
1. Добавление характеристических тестов
Характеристические тесты (characterization tests) фиксируют текущее поведение кода. Они пишутся после того, как код написан, и позволяют убедиться, что рефакторинг не изменил поведение.
<?php
// Пример характеристического теста для legacy функции
class LegacyCalculatorTest extends PHPUnit\Framework\TestCase
{
public function testCalculateTotal()
{
// Фиксируем текущее поведение
$result = legacy_calculate_total(100, 0.2);
$this->assertEquals(120, $result);
}
}
2. Создание "одеяла" тестов
Начните с интеграционных тестов, которые проверяют ключевые пользовательские сценарии (регистрация, оформление заказа и т.д.). Это даст базовую уверенность при рефакторинге.
Пошаговая стратегия рефакторинга legacy PHP кода
Используем подход "Strangler Fig Pattern" (удушающий инжир) — постепенно заменяем старые компоненты новыми, не останавливая работу системы.
Шаг 1: Изолируйте и инкапсулируйте глобальное состояние
Глобальные переменные и функции — главный враг рефакторинга. Начните с их инкапсуляции:
<?php
// ДО: глобальная функция и переменная
$db = mysql_connect('localhost', 'user', 'pass');
function get_user($id) {
global $db;
$result = mysql_query("SELECT * FROM users WHERE id = $id", $db);
return mysql_fetch_assoc($result);
}
// ПОСЛЕ: класс с явной зависимостью
class UserRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function findById(int $id): ?array {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch();
return $user ?: null;
}
}
Шаг 2: Отделите логику от представления
Типичный legacy код содержит SQL и PHP-логику прямо в HTML. Используйте паттерн "Шаблонный метод" для разделения:
<?php
// ДО: спагетти-код в шаблоне
?>
<ul>
<?php
$result = mysql_query("SELECT * FROM products");
while ($row = mysql_fetch_assoc($result)) {
echo "<li>" . $row['name'] . " - " . $row['price'] . " руб.</li>";
}
?>
</ul>
// ПОСЛЕ: выделение логики в сервис
class ProductService {
public function __construct(private ProductRepository $repo) {}
public function getProductList(): array {
return $this->repo->findAll();
}
}
// В шаблоне остаётся только вывод
// <?php foreach ($products as $product): ?>
// <li><?= $product['name'] ?> - <?= $product['price'] ?> руб.</li>
// <?php endforeach; ?>
Шаг 3: Замените устаревшие функции и конструкции
Постепенно заменяйте функции из "чёрного списка" PHP:
mysql_*→PDOилиmysqliereg/eregi→preg_matchsplit→explodeилиpreg_splitcall_user_func→ современные callable-типы
<?php
// ДО: устаревший подход
$result = mysql_query("SELECT * FROM orders WHERE status = 'active'");
while ($row = mysql_fetch_assoc($result)) {
// обработка
}
// ПОСЛЕ: современный PDO
$stmt = $pdo->prepare("SELECT * FROM orders WHERE status = :status");
$stmt->execute(['status' => 'active']);
foreach ($stmt as $row) {
// обработка
}
Шаг 4: Внедрите Dependency Injection (DI)
Избавьтесь от создания объектов внутри классов. Используйте внедрение зависимостей через конструктор:
<?php
// ДО: жёсткая связность
class OrderProcessor {
private $mailer;
public function __construct() {
$this->mailer = new Mailer('smtp.example.com');
}
}
// ПОСЛЕ: DI
class OrderProcessor {
public function __construct(
private MailerInterface $mailer,
private LoggerInterface $logger
) {}
}
// Использование с DI-контейнером
$processor = new OrderProcessor(new SmtpMailer(), new FileLogger());