Рефакторинг legacy кода на PHP: от А до Я для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Рефакторинг 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 или mysqli
  • ereg / eregi → preg_match
  • split → explode или preg_split
  • call_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());

Безопасный рефакторинг: техники и инструменты

Блоги

Рекомендаций по книгам