SOLID принципы в PHP: теория и практика от А до Я

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

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

Начать курс

Введение: почему SOLID — это фундамент качественного PHP-кода

Каждый PHP-разработчик рано или поздно сталкивается с ситуацией, когда небольшой проект превращается в "спагетти-код". Классы становятся перегруженными, изменения в одной части системы ломают другую, а тестирование превращается в кошмар. Именно для решения этих проблем Роберт Мартин (дядя Боб) сформулировал пять принципов SOLID. Эти принципы — не абстрактная теория, а практические инструменты, которые помогают создавать гибкий, поддерживаемый и тестируемый код на PHP.

В этой статье мы разберём каждый принцип SOLID: от теории до реальных примеров на PHP 8.x. Вы узнаете, как применять их в повседневной разработке и каких антипаттернов следует избегать.

1. Single Responsibility Principle (SRP) — принцип единственной ответственности

Теория

Принцип гласит: у класса должна быть только одна причина для изменения. Это означает, что каждый класс должен отвечать за одну конкретную функциональность или бизнес-логику.

Антипаттерн (нарушение SRP)

<?php
class OrderProcessor {
    public function process(Order $order): void {
        // 1. Валидация заказа
        if (empty($order->getItems())) {
            throw new \InvalidArgumentException('Order has no items');
        }
        
        // 2. Сохранение в БД
        $db = new \PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
        $stmt = $db->prepare('INSERT INTO orders ...');
        $stmt->execute([...]);
        
        // 3. Отправка email
        mail($order->getCustomerEmail(), 'Заказ подтверждён', '...');
        
        // 4. Логирование
        file_put_contents('/var/log/orders.log', 'Order processed: '.$order->getId());
    }
}
?>

Здесь класс OrderProcessor делает слишком много: валидирует, работает с БД, отправляет email, логирует. Нарушение SRP ведёт к тому, что изменение логики отправки email потребует изменения класса, отвечающего за обработку заказов.

Рефакторинг с соблюдением SRP

<?php
class OrderValidator {
    public function validate(Order $order): void {
        if (empty($order->getItems())) {
            throw new \InvalidArgumentException('Order has no items');
        }
    }
}

class OrderRepository {
    public function save(Order $order): void {
        // Логика сохранения в БД
    }
}

class EmailNotifier {
    public function sendOrderConfirmation(Order $order): void {
        // Логика отправки email
    }
}

class Logger {
    public function log(string $message): void {
        // Логирование
    }
}

class OrderProcessor {
    public function __construct(
        private OrderValidator $validator,
        private OrderRepository $repository,
        private EmailNotifier $notifier,
        private Logger $logger
    ) {}
    
    public function process(Order $order): void {
        $this->validator->validate($order);
        $this->repository->save($order);
        $this->notifier->sendOrderConfirmation($order);
        $this->logger->log('Order processed: '.$order->getId());
    }
}
?>

Теперь каждый класс имеет единственную ответственность. Изменение логики email не затрагивает работу с БД.

2. Open-Closed Principle (OCP) — принцип открытости/закрытости

Теория

Программные сущности должны быть открыты для расширения, но закрыты для модификации. Другими словами, вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

Антипаттерн

<?php
class PaymentProcessor {
    public function process(string $type, float $amount): void {
        if ($type === 'credit_card') {
            // Обработка кредитной карты
        } elseif ($type === 'paypal') {
            // Обработка PayPal
        } elseif ($type === 'crypto') {
            // Обработка криптовалюты
        }
        // При добавлении нового типа придётся менять этот класс!
    }
}
?>

Решение через полиморфизм

<?php
interface PaymentMethod {
    public function process(float $amount): void;
}

class CreditCardPayment implements PaymentMethod {
    public function process(float $amount): void {
        // Логика для кредитной карты
    }
}

class PayPalPayment implements PaymentMethod {
    public function process(float $amount): void {
        // Логика для PayPal
    }
}

class CryptoPayment implements PaymentMethod {
    public function process(float $amount): void {
        // Логика для криптовалюты
    }
}

class PaymentProcessor {
    public function process(PaymentMethod $method, float $amount): void {
        $method->process($amount);
    }
}

// Использование:
$processor = new PaymentProcessor();
$processor->process(new CreditCardPayment(), 100.0);
$processor->process(new CryptoPayment(), 0.5);
?>

Теперь добавление нового способа оплаты требует лишь создания нового класса, реализующего интерфейс PaymentMethod.

3. Liskov Substitution Principle (LSP) — принцип подстановки Барбары Лисков

Теория

Объекты в программе должны быть заменимы экземплярами их подтипов без нарушения корректности программы. Проще говоря: если у вас есть функция, работающая с родительским классом, она должна корректно работать и с любым его наследником.

Нарушение LSP

<?php
class Rectangle {
    public function __construct(
        protected int $width,
        protected int $height
    ) {}
    
    public function setWidth(int $width): void {
        $this->width = $width;
    }
    
    public function setHeight(int $height): void {
        $this->height = $height;
    }
    
    public function getArea(): int {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle {
    public function setWidth(int $width): void {
        $this->width = $width;
        $this->height = $width; // Нарушение: квадрат меняет высоту при изменении ширины
    }
    
    public function setHeight(int $height): void {
        $this->height = $height;
        $this->width = $height;
    }
}

function printArea(Rectangle $rect): void {
    $rect->setWidth(5);
    $rect->setHeight(10);
    echo $rect->getArea(); // Ожидаем 50, но для Square получим 100!
}
?>

Правильное решение

Блоги

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