SOLID принципы в PHP: продвинутое применение и практические примеры

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

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

Начать курс

Введение: Почему SOLID — это не просто догма, а инструмент

Большинство разработчиков знают, что SOLID — это пять принципов объектно-ориентированного программирования: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation и Dependency Inversion. Однако на практике понимание часто сводится к механическому следованию правилам. В этой статье мы пойдем дальше: разберем продвинутые сценарии применения SOLID в PHP, рассмотрим антипаттерны, которые нарушают принципы, и покажем, как рефакторинг с учетом SOLID делает код гибким и тестируемым.

Материал рассчитан на разработчиков уровня Middle+ и Senior, которые уже пишут на PHP и хотят улучшить архитектуру своих проектов.

1. Принцип единственной ответственности (SRP): когда класс «знает слишком много»

Классическая формулировка: «У класса должна быть только одна причина для изменения». На продвинутом уровне важно понимать, что «причина изменения» — это бизнес-требование. Если ваш класс изменяется из-за изменения правил валидации, формата вывода и логики сохранения — это нарушение SRP.

Пример нарушения (God Object)

class OrderProcessor {
    public function process(array $data): void {
        // 1. Валидация данных
        if (empty($data['user_id']) || empty($data['items'])) {
            throw new InvalidArgumentException('Invalid order data');
        }
        
        // 2. Подсчет стоимости
        $total = 0;
        foreach ($data['items'] as $item) {
            $total += $item['price'] * $item['quantity'];
        }
        
        // 3. Сохранение в БД
        $sql = "INSERT INTO orders (user_id, total, status) VALUES (?, ?, 'pending')";
        // ... выполнение запроса
        
        // 4. Отправка email
        mail($data['user_email'], 'Order confirmation', "Total: $total");
    }
}

Рефакторинг с SRP

Разделяем на три класса с четкими зонами ответственности:

class OrderValidator {
    public function validate(array $data): void {
        if (empty($data['user_id']) || empty($data['items'])) {
            throw new InvalidArgumentException('Invalid order data');
        }
    }
}

class OrderCalculator {
    public function calculateTotal(array $items): float {
        return array_reduce($items, function ($carry, $item) {
            return $carry + $item['price'] * $item['quantity'];
        }, 0.0);
    }
}

class OrderRepository {
    public function save(int $userId, float $total, string $status = 'pending'): int {
        // Логика сохранения в БД
        return 42; // ID заказа
    }
}

class OrderProcessor {
    public function __construct(
        private OrderValidator $validator,
        private OrderCalculator $calculator,
        private OrderRepository $repository,
        private Mailer $mailer
    ) {}
    
    public function process(array $data): void {
        $this->validator->validate($data);
        $total = $this->calculator->calculateTotal($data['items']);
        $orderId = $this->repository->save($data['user_id'], $total);
        $this->mailer->send($data['user_email'], 'Order #' . $orderId);
    }
}

2. Принцип открытости/закрытости (OCP): расширение без модификации

Суть OCP: классы должны быть открыты для расширения, но закрыты для изменения. На практике это означает использование полиморфизма и абстракций вместо условных конструкций.

Нарушение OCP: switch-case на каждый новый тип

class PaymentProcessor {
    public function pay(string $type, float $amount): void {
        switch ($type) {
            case 'credit_card':
                // обработка карты
                break;
            case 'paypal':
                // обработка PayPal
                break;
            // При добавлении нового типа — меняем класс!
        }
    }
}

Решение через стратегию (Strategy Pattern)

interface PaymentMethod {
    public function process(float $amount): bool;
}

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

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

// Новый тип — просто новый класс, без изменения существующего кода
class CryptoPayment implements PaymentMethod {
    public function process(float $amount): bool {
        // Логика для криптовалюты
        return true;
    }
}

class PaymentProcessor {
    public function __construct(
        private PaymentMethod $method
    ) {}
    
    public function execute(float $amount): bool {
        return $this->method->process($amount);
    }
}

3. Принцип подстановки Лисков (LSP): контрактное программирование

LSP часто нарушают при наследовании. Формально: «Объекты базового класса должны заменяться объектами производного класса без изменения свойств программы». На PHP это особенно актуально при работе с ковариантностью и контравариантностью.

Типичное нарушение: ослабление предусловий

class Rectangle {
    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 {
        parent::setWidth($width);
        parent::setHeight($width); // Нарушение: квадрат меняет высоту!
    }
    
    public function setHeight(int $height): void {
        parent::setWidth($height);
        parent::setHeight($height);
    }
}

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

Правильное решение: композиция вместо наследования

interface Shape {
    public function getArea(): int;
}

class Rectangle implements Shape {
    public function __construct(
        private int $width,
        private int $height
    ) {}
    
    public function getArea(): int {
        return $this->width * $this->height;
    }
}

class Square implements Shape {
    public function __construct(
        private int $side
    ) {}
    
    public function getArea(): int {
        return $this->side * $this->side;
    }
}

4. Принцип разделения интерфейсов (ISP): толстые интерфейсы — зло

ISP гласит: «Клиенты не должны зависеть от методов, которые они не используют». В PHP это особенно важно при работе с типизированными свойствами и readonly-классами.

Пример «толстого» интерфейса

interface Worker {
    public function work(): void;
    public function eat(): void;
    public function sleep(): void;
}

class HumanWorker implements Worker {
    public function work(): void { /* работает */ }
    public function                

Блоги

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