Наследование в PHP: когда extends оправдан, а когда это костыль
Унаследовать. Переопределить. Специализировать.
Унаследовали всё общее у родителя через extends, переопределили то, что работает иначе, через parent:: дополняя, а не дублируя, и получили класс, который является настоящей разновидностью родителя — а не копией с другим именем.
Зачем это нужно
Наследование — второй кит ООП. Суть в трёх словах: унаследовать общее у родителя, переопределить то, что работает иначе, и специализировать класс под свою задачу. Ключевое слово здесь — «добавляет»: наследование описывает отношение «это разновидность того», а не «мне нужно позаимствовать пару методов».
Почему об этом стоит помнить: именно на наследовании чаще всего ломают архитектуру. Бывает так: нужен доступ к методам другого класса — и рука тянется написать extends. Вроде работает, код короче. А через полгода классы сцеплены намертво: поменял логику в родителе — у всех наследников что-то отвалилось, и ты не знаешь, у кого именно. Наследование — мощный инструмент, но с узкой зоной применения.
Как это работает
Синтаксис простой: extends — и наследник получает всё public и protected от родителя.
phpclass Animal
{
public function __construct(
protected string $name
) {}
public function info(): string
{
return "Меня зовут {$this->name}";
}
}
class Dog extends Animal
{
public function info(): string
{
return parent::info() . ', и я говорю «гав»';
}
}
echo (new Dog('Рекс'))->info(); // Меня зовут Рекс, и я говорю «гав»
Здесь два движения:
- наследник переопределяет метод
info()— меняет поведение под себя; - через
parent::info()дополняет родительскую логику, а не дублирует её заново.
Тест на оправданность: «это разновидность»
Прежде чем писать extends, задайте один вопрос: «наследник — это разновидность родителя?»
phpclass Controller extends BaseController {} // да, контроллер — разновидность BaseController
class AdminController extends Controller {} // да, админский — разновидность контроллера
class ReportService extends DbService {} // ❌ отчёт — НЕ разновидность сервиса БД
С контроллерами всё честно: базовый Controller даёт общую инфраструктуру (middleware, авторизацию, валидацию), а конкретный контроллер — его разновидность.
А вот ReportService extends DbService — красный флаг. Сервису отчётов нужен доступ к базе, но сам он базой не является. Ему нужно не наследование, а композиция — получить зависимость через конструктор:
php// ПЛОХО: наследование ради доступа к методам
class ReportService extends DbService
{
public function sales(): array
{
return $this->query('...'); // метод из родителя
}
}
// ХОРОШО: композиция — нужен функционал, а не «родство»
class ReportService
{
public function __construct(
private DbService $db
) {}
public function sales(): array
{
return $this->db->query('...');
}
}
Разница тонкая, но важная. Наследование говорит: «я — это он, всё моё — его тоже». Композиция говорит: «я пользуюсь его помощью». Второе — почти всегда то, что имелось в виду.
Как это выглядит в Laravel
Laravel сам ставит пример правильного наследования — и правильного отказа от него.
Наследование оправдано:
php// Иерархия исключений — классика «разновидностей»
class OrderException extends Exception {}
class PaymentFailedException extends OrderException {}
// Роут-контроллеры расширяют базовый
class OrderController extends Controller
{
public function index(): View { /* ... */ }
}
Исключение — это разновидность исключения. Контроллер — разновидность контроллера. Логично.
А вот модели — интересный случай. Каждая модель наследует Model, но это единственный уровень — глубже не идём:
phpclass Order extends Model { /* ... */ }
// class RefundedOrder extends Order {} // ❌ так делать не надо
Искушение сделать RefundedOrder extends Order велико: «перевозим всю логику заказов бесплатно». Но тогда все методы Order будут жить в RefundedOrder, включая те, которые для возврата не имеют смысла. Правильнее — тот же самый класс Order с методами-состояниями (помните инкапсуляцию? refund(), ship(), cancel()), а не параллельная иерархия классов.
Хрупкий родитель: главная цена
У наследования есть скрытая цена, о которой новичок узнаёт на своей шкуре. Пока иерархия маленькая — всё хорошо. Но родительский класс с течением времени меняется:
phpclass BasePayment
{
public function pay(float $amount): bool
{
$this->log($amount); // добавили логирование в новой версии
return $this->doPay($amount);
}
}
Вы обновили библиотеку, где этот класс лежит, — и логирование вдруг начало писать в базу, которой нет в тестовой среде. Вы не меняли ни строчки своего кода, а поведение сломалось. Это хрупкий базовый класс: любой, кто поменяет родителя, меняет всех наследников, и часто — непредсказуемым для них образом.
Композиция от этого защищает: вы зависите от интерфейса, а не от живого организма родителя.
Плохо / Хорошо
| Плохо | Хорошо |
|---|---|
extends ради доступа к паре методов | Композиция: зависимость через конструктор |
| Иерархия в 3–4 уровня «иначе код дублируется» | Трейты для утилит, сервисы для логики |
RefundedOrder extends Order ради переноса логики | Один класс Order с методами-состояниями |
| Слепое доверие к родительскому классу | Понимание, что родитель меняется — и наследники с ним |
Подводный камень
- «Наследование = переиспользование» — ложное уравнение. Реиспользование кода дают трейты, композиция и сервисы. Наследование — для иерархий «разновидностей», и почти ничего больше.
- Глубокие цепочки — красный флаг. Три-четыре уровня
extends— почти всегда признак того, что логику пора выносить в отдельные классы и композицию. parent::не гарантирован. Родительский метод может измениться при обновлении — и ваши дополнения к нему будут работать не так, как вы рассчитывали.- Не забывайте
final. Если класс не рассчитан на наследование — пометьте егоfinal. Это честная коммуникация: «не строй иерархию на мне».
Мини-шпаргалка
php// Наследование — для «разновидностей»
class PaymentFailedException extends OrderException {}
// Переопределение + дополнение родителя
class Dog extends Animal
{
public function info(): string
{
return parent::info() . ', и я говорю «гав»';
}
}
// Композиция вместо наследования ради методов
class ReportService
{
public function __construct(private DbService $db) {}
}
// Запрет наследования
final class Money { /* ... */ }
Три вывода
- Наследование — отношение «это разновидность», а не способ «взять методы».
- Композиция (зависимость через конструктор) почти всегда дешевле и надёжнее наследования — и легче меняется.
- Изменение родителя меняет всех наследников — поэтому иерархии держите плоскими, а ненаследуемые классы помечайте
final.