🐘 PHP

OOP: Vererbung in PHP: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты решил создать свою собственную видеоигру. В этой игре есть разные персонажи: эльфы, орки, маги и рыцари. У каждого из них есть что-то общее: все они могут ходить, бегать, прыгать, у них есть здоровье, уровень и инвентарь. Но при этом у каждого есть свои уникальные способности: эльфы стреляют из лука, орки наносят сокрушительные удары, маги колдуют заклинания, а рыцари носят тяжёлую броню.

Если бы ты писал код для каждого персонажа с нуля, тебе пришлось бы сотни раз повторять одно и то же: описывать, как работает ходьба, как хранится здоровье, как персонаж получает урон. Это было бы ужасно долго, скучно и очень легко ошибиться. А если бы ты захотел добавить новую общую способность (например, возможность сидеть у костра и восстанавливать силы), тебе пришлось бы менять код каждого персонажа по отдельности!

Вот тут на помощь и приходит наследование — один из четырёх «китов» объектно-ориентированного программирования (ООП).

Давай проведём аналогию из реальной жизни. Представь себе чертёж автомобиля. На этом чертеже есть всё самое главное: колёса, руль, двигатель, педали, сиденья. Это — базовый класс (или родительский класс). Теперь ты хочешь сделать конкретные модели: «Седан», «Внедорожник» и «Спорткар». Ты не рисуешь каждый раз новый чертёж с нуля! Ты берёшь базовый чертёж и говоришь: «Это будет автомобиль, но у него будет ещё и большой багажник (для Седана)», «У него будет полный привод и высокий клиренс (для Внедорожника)», «У него будет мощный турбодвигатель и антикрыло (для Спорткара)».

В программировании это работает точно так же. Ты создаёшь один родительский класс (например, Персонаж), в котором описываешь все общие свойства (здоровье, имя, уровень) и методы (ходить, бежать, атаковать). А затем создаёшь дочерние классы (Эльф, Орк, Маг), которые «наследуют» всё это добро и добавляют что-то своё.

💡 Важно: Наследование — это не копирование кода. Это создание иерархии. Дочерний класс автоматически получает всё, что есть у родителя, но может это изменить (переопределить) или дополнить.

Зачем это нужно? Вот главные причины:

  • Экономия времени: Не нужно писать один и тот же код по сто раз.
  • Лёгкость поддержки: Если ты нашёл ошибку в общем методе (например, в расчёте урона), ты исправляешь её один раз в родительском классе, и она автоматически исправляется у всех наследников.
  • Расширяемость: Хочешь добавить нового персонажа (например, Гнома)? Просто создай новый класс-наследник и опиши только его уникальные черты. Всё остальное уже готово!
  • Логичность: Код становится похож на реальный мир, где есть общие категории и частные случаи. Это легче понимать.

Теория: как это работает изнутри

Давай заглянем «под капот» и разберёмся, что на самом деле происходит в памяти компьютера, когда мы используем наследование.

Когда ты создаёшь объект от дочернего класса (например, $legolas = new Elf()), PHP не создаёт копию родительского класса. Вместо этого он строит цепочку наследования. Объект «знает», что он принадлежит классу Elf, который, в свою очередь, «расширяет» класс Character.

Когда ты вызываешь метод у объекта (например, $legolas->walk()), PHP делает следующее:

  1. Сначала он ищет метод walk() в классе Elf.
  2. Если он его там не находит, он поднимается на уровень выше — в класс Character.
  3. Если метод найден в Character, он его выполняет.
  4. Если метода нет и там, PHP поднимается ещё выше (если есть «дедушка») или выдаёт ошибку.

То же самое происходит со свойствами (переменными класса). Если ты объявил свойство $health в родительском классе, оно будет доступно во всех дочерних классах, даже если ты его там не объявлял заново.

Ключевое слово, которое делает всю магию в PHP — это extends (расширяет). Оно буквально говорит: «Этот класс является расширением того класса».

Важно понимать разницу между модификаторами доступа:

Модификатор Доступ в родительском классе Доступ в дочернем классе Доступ извне (из кода)
public ✅ Да ✅ Да ✅ Да
protected ✅ Да ✅ Да ❌ Нет
private ✅ Да ❌ Нет ❌ Нет
⚠️ Запомни раз и навсегда: private — это «личный дневник» класса. Даже дети (наследники) не имеют права в него заглядывать. Если ты хочешь, чтобы свойство было доступно наследникам, но скрыто от внешнего мира, используй protected.

Ещё одно важное понятие — переопределение методов (Overriding). Дочерний класс может объявить метод с тем же именем, что и у родителя. В этом случае метод родителя «перезаписывается». Если внутри дочернего метода тебе всё же нужно вызвать родительскую версию (например, чтобы не дублировать код), используй конструкцию parent::имяМетода().

Синтаксис в PHP: все формы и варианты

Давай разберём синтаксис наследования в PHP от простого к сложному. В PHP используется одиночное наследование — один класс может наследовать только от одного родителя. Однако, родитель может иметь много детей, и можно строить целые цепочки: Дед -> Отец -> Сын.

1. Базовое наследование

Самый простой случай. Класс-наследник использует ключевое слово extends.

<?php
// Родительский класс
class Animal {
    public $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function makeSound() {
        return "Издаёт звук";
    }
}

// Дочерний класс. Он наследует всё от Animal.
class Dog extends Animal {
    // Мы можем добавить новый метод
    public function fetch() {
        return $this->name . " приносит палку!";
    }
}

$dog = new Dog("Бобик");
echo $dog->name;        // Выведет: Бобик (унаследовано)
echo $dog->makeSound(); // Выведет: Издаёт звук (унаследовано)
echo $dog->fetch();     // Выведет: Бобик приносит палку! (свой метод)
?>

2. Переопределение методов (Overriding)

Дочерний класс может изменить поведение родительского метода.

<?php
class Animal {
    public function makeSound() {
        return "Какой-то звук";
    }
}

class Cat extends Animal {
    // Переопределяем родительский метод
    public function makeSound() {
        return "Мяу!";
    }
}

class Cow extends Animal {
    public function makeSound() {
        return "Мууу!";
    }
}

$animals = [new Cat(), new Cow(), new Animal()];
foreach ($animals as $animal) {
    echo $animal->makeSound() . "
";
}
// Выведет:
// Мяу!
// Мууу!
// Какой-то звук
?>

3. Вызов родительского метода (parent::)

Часто нужно не полностью заменить метод, а дополнить его. Для этого используется parent::.

<?php
class Car {
    protected $speed = 0;

    public function accelerate() {
        $this->speed += 10;
        return "Скорость увеличена до " . $this->speed;
    }
}

class SportCar extends Car {
    public function accelerate() {
        // Сначала вызываем родительский метод
        $result = parent::accelerate();
        // А потом добавляем свою логику
        $this->speed += 20; // Спорткар разгоняется быстрее!
        return $result . ", а затем до " . $this->speed;
    }
}

$ferrari = new SportCar();
echo $ferrari->accelerate(); // Скорость увеличена до 10, а затем до 30
?>

4. Наследование конструктора

Конструкторы тоже наследуются, но их часто переопределяют. Если ты переопределяешь конструктор, родительский конструктор не вызывается автоматически. Его нужно вызвать вручную через parent::__construct().

<?php
class Person {
    protected $name;
    protected $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

class Student extends Person {
    private $studentId;

    public function __construct($name, $age, $studentId) {
        // Обязательно вызываем родительский конструктор!
        parent::__construct($name, $age);
        $this->studentId = $studentId;
    }

    public function getInfo() {
        return "Студент: {$this->name}, возраст: {$this->age}, ID: {$this->studentId}";
    }
}

$student = new Student("Иван", 20, "ST12345");
echo $student->getInfo(); // Студент: Иван, возраст: 20, ID: ST12345
?>

5. Запрет наследования (final)

Иногда ты хочешь сказать: «Этот класс — конечный, его нельзя наследовать» или «Этот метод нельзя переопределять». Для этого используется ключевое слово final.

<?php
// Этот класс нельзя наследовать!
final class MathUtils {
    public static function add($a, $b) {
        return $a + $b;
    }
}

// Следующий код вызовет ошибку:
// class AdvancedMath extends MathUtils {} // Fatal error!

class BaseClass {
    // Этот метод нельзя переопределить в дочерних классах
    final public function secretAlgorithm() {
        return "Секретный алгоритм";
    }
}

class ChildClass extends BaseClass {
    // Следующий код вызовет ошибку:
    // public function secretAlgorithm() { return "Новый"; } // Fatal error!
}
?>

6. Абстрактные классы

Абстрактный класс — это класс, который запрещено создавать напрямую. Он существует только для того, чтобы быть родителем. Он может содержать абстрактные методы — методы без реализации, которые обязаны реализовать все наследники.

<?php
// Абстрактный класс. Нельзя создать $shape = new Shape()!
abstract class Shape {
    protected $color;

    public function __construct($color) {
        $this->color = $color;
    }

    // Абстрактный метод. У него нет тела!
    // Все наследники ОБЯЗАНЫ его реализовать.
    abstract public function getArea();

    // Обычный метод. Его можно не переопределять.
    public function getColor() {
        return $this->color;
    }
}

class Circle extends Shape {
    private $radius;

    public function __construct($color, $radius) {
        parent::__construct($color);
        $this->radius = $radius;
    }

    // Обязаны реализовать getArea()
    public function getArea() {
        return pi() * $this->radius * $this->radius;
    }
}

class Square extends Shape {
    private $side;

    public function __construct($color, $side) {
        parent::__construct($color);
        $this->side = $side;
    }

    public function getArea() {
        return $this->side * $this->side;
    }
}

$circle = new Circle("Красный", 5);
echo $circle->getArea(); // 78.539816339745
echo $circle->getColor(); // Красный
?>

Примеры кода — от простого к сложному

Давай закрепим теорию на семи практических примерах. Каждый пример — рабочий скрипт с подробными комментариями.

Пример 1: Базовое наследование «Животные»

<?php
/**
 * Пример 1: Самое простое наследование.
 * Создаём общий класс "Животное" и два дочерних: "Собака" и "Кошка".
 */

// Родительский класс
class Animal {
    public $name;

    // Конструктор: вызывается при создании объекта
    public function __construct($name) {
        $this->name = $name;
    }

    // Общий метод для всех животных
    public function speak() {
        return $this->name . " издаёт звук.";
    }
}

// Дочерний класс "Собака"
class Dog extends Animal {
    // Переопределяем метод speak для собак
    public function speak() {
        return $this->name . " говорит: Гав-гав!";
    }
}

// Дочерний класс "Кошка"
class Cat extends Animal {
    public function speak() {
        return $this->name . " говорит: Мяу!";
    }
}

// Создаём объекты
$animals = [
    new Dog("Бобик"),
    new Cat("Мурка"),
    new Animal("Неизвестный зверь")
];

// Выводим звуки каждого животного
foreach ($animals as $animal) {
    echo $animal->speak() . "
";
}
// Вывод:
// Бобик говорит: Гав-гав!
// Мурка говорит: Мяу!
// Неизвестный зверь издаёт звук.
?>

Пример 2: Наследование с добавлением новых свойств

<?php
/**
 * Пример 2: Добавляем новые свойства в дочерний класс.
 * Базовый класс "Транспорт" и дочерний "Электромобиль".
 */

class Vehicle {
    protected $brand;
    protected $speed = 0;

    public function __construct($brand) {
        $this->brand = $brand;
    }

    public function accelerate() {
        $this->speed += 10;
        return "Скорость: " . $this->speed . " км/ч";
    }
}

// Электромобиль наследует всё от Vehicle
class ElectricCar extends Vehicle {
    // Новое свойство, которого нет у обычного транспорта
    private $batteryLevel = 100;

    // Новый метод, специфичный для электромобилей
    public function charge() {
        $this->batteryLevel = 100;
        return "Батарея заряжена до 100%";
    }

    // Переопределяем метод accelerate, добавляя логику расхода батареи
    public function accelerate() {
        if ($this->batteryLevel <= 0) {
            return "Батарея разряжена! Зарядите автомобиль.";
        }
        $this->batteryLevel -= 5;
        // Вызываем родительский метод, чтобы не писать логику увеличения скорости заново
        return parent::accelerate() . " (Заряд батареи: {$this->batteryLevel}%)";
    }
}

$tesla = new ElectricCar("Tesla");
echo $tesla->accelerate() . "
"; // Скорость: 10 км/ч (Заряд батареи: 95%)
echo $tesla->charge() . "
";     // Батарея заряжена до 100%
?>

Пример 3: Цепочка наследования (дедушка, отец, сын)

<?php
/**
 * Пример 3: Многоуровневое наследование.
 * Создаём иерархию: Работник -> Программист -> Бэкенд-разработчик.
 */

class Employee {
    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function work() {
        return "{$this->name} работает.";
    }
}

class Programmer extends Employee {
    protected $language;

    public function __construct($name, $language) {
        parent::__construct($name);
        $this->language = $language;
    }

    public function code() {
        return "{$this->name} пишет код на {$this->language}.";
    }
}

class BackendDeveloper extends Programmer {
    private $framework;

    public function __construct($name, $language, $framework) {
        // Вызываем конструктор Programmer, который вызывает конструктор Employee
        parent::__construct($name, $language);
        $this->framework = $framework;
    }

    public function buildAPI() {
        return "{$this->name} создаёт API на фреймворке {$this->framework}.";
    }
}

$dev = new BackendDeveloper("Анна", "PHP", "Laravel");
echo $dev->work() . "
";      // Анна работает. (унаследовано от Employee)
echo $dev->code() . "
";      // Анна пишет код на PHP. (унаследовано от Programmer)
echo $dev->buildAPI() . "
";  // Анна создаёт API на фреймворке Laravel. (свой метод)
?>

Пример 4: Абстрактный класс «Платежная система»

<?php
/**
 * Пример 4: Абстрактный класс гарантирует,
 * что у всех платёжных систем будет метод processPayment().
 */

abstract class PaymentSystem {
    protected $apiKey;

    public function __construct($apiKey) {
        $this->apiKey = $apiKey;
    }

    // Все наследники обязаны реализовать этот метод
    abstract public function processPayment($amount);

    // Общий метод для логирования
    public function logTransaction($amount) {
        return "Платёж на сумму $amount обработан.";
    }
}

class PayPal extends PaymentSystem {
    public function processPayment($amount) {
        // Специфическая логика для PayPal
        return "PayPal: Платёж на $$amount выполнен. Ключ: {$this->apiKey}";
    }
}

class Stripe extends PaymentSystem {
    public function processPayment($amount) {
        return "Stripe: Платёж на $$amount выполнен. Комиссия 2%.";
    }
}

$payments = [new PayPal("key123"), new Stripe("key456")];
foreach ($payments as $payment) {
    echo $payment->processPayment(100) . "
";
}
// PayPal: Платёж на $100 выполнен. Ключ: key123
// Stripe: Платёж на $100 выполнен. Комиссия 2%.
?>

Пример 5: Использование final для защиты методов

<?php
/**
 * Пример 5: Запрещаем переопределение критически важного метода.
 */

class DatabaseConnection {
    // Этот метод нельзя переопределить в наследниках!
    final public function connect() {
        return "Устанавливаем безопасное соединение с базой данных...";
    }

    public function query($sql) {
        return "Выполняем запрос: $sql";
    }
}

class MySQLDatabase extends DatabaseConnection {
    // Попытка переопределить connect() вызовет фатальную ошибку
    // public function connect() { ... } // Ошибка!

    // А этот метод переопределить можно
    public function query($sql) {
        return "MySQL: " . parent::query($sql);
    }
}

$db = new MySQLDatabase();
echo $db->connect() . "
"; // Устанавливаем безопасное соединение...
echo $db->query("SELECT * FROM users"); // MySQL: Выполняем запрос: SELECT * FROM users
?>

Пример 6: Наследование и статические методы

<?php
/**
 * Пример 6: Статические методы и свойства тоже наследуются.
 */

class MathHelper {
    public static $pi = 3.14159;

    public static function square($x) {
        return $x * $x;
    }
}

class AdvancedMath extends MathHelper {
    // Переопределяем статическое свойство
    public static $pi = 3.1415926535;

    // Добавляем новый статический метод
    public static function cube($x) {
        return $x * $x * $x;
    }
}

echo MathHelper::square(5) . "
";       // 25
echo AdvancedMath::square(5) . "
";     // 25 (унаследован)
echo AdvancedMath::cube(3) . "
";       // 27 (свой)
echo AdvancedMath::$pi . "
";           // 3.1415926535 (переопределён)
?>

Пример 7: Проверка принадлежности к классу (instanceof)

<?php
/**
 * Пример 7: Оператор instanceof проверяет, принадлежит ли объект
 * к определённому классу или его наследникам.
 */

class User {}
class Admin extends User {}
class SuperAdmin extends Admin {}

$user = new User();
$admin = new Admin();
$superAdmin = new SuperAdmin();

// Проверяем
echo ($user instanceof User) ? "Да" : "Нет";        // Да
echo "
";
echo ($admin instanceof User) ? "Да" : "Нет";       // Да (Admin наследует User)
echo "
";
echo ($superAdmin instanceof Admin) ? "Да" : "Нет"; // Да (SuperAdmin наследует Admin)
echo "
";
echo ($user instanceof Admin) ? "Да" : "Нет";       // Нет (User не является Admin)
?>

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python или планируешь изучать его, полезно знать, как наследование реализовано в разных языках. PHP и Python — оба являются объектно-ориентированными, но у них есть важные различия.

Характеристика PHP Python
Ключевое слово для наследования extends Класс-родитель указывается в скобках: class Dog(Animal):
Множественное наследование ❌ Нет. Класс может наследовать только от одного родителя. ✅ Да. Класс может наследовать от нескольких классов: class Bat(Mammal, Winged):
Вызов родительского метода parent::methodName() super().method_name() или ParentClass.method_name(self)
Модификаторы доступа public, protected, private (строгие) Нет ключевых слов. Используется соглашение: _protected (одно подчёркивание) и __private (два подчёркивания). Это не защита, а «джентельменское соглашение».
Абстрактные классы Используется ключевое слово abstract перед классом и методами. Используется модуль abc (Abstract Base Classes). Класс наследует от ABC, методы декорируются @abstractmethod.
Интерфейсы Есть отдельное ключевое слово interface и implements. Формально нет. Роль интерфейсов выполняют абстрактные классы (ABC).
Конструктор __construct() __init__()
💡 Совет: Отсутствие множественного наследования в PHP — это не баг, а фича. Разработчики PHP сознательно отказались от него, потому что он часто приводит к путанице (проблема «ромба»). Вместо этого в PHP используются трейты (Trait) — механизм для повторного использования кода без наследования.

Пример наследования в Python для сравнения:

# Python код для сравнения
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} издаёт звук."

class Dog(Animal):  # Наследование через скобки
    def speak(self):
        return f"{self.name} говорит: Гав!"

# Множественное наследование (недоступно в PHP)
class FlyingDog(Dog, Bird):  # Гипотетический пример
    pass

Типичные ошибки и подводные камни

Даже опытные программисты иногда спотыкаются на наследовании. Вот список самых частых ошибок, которые совершают новички (и не только).

Ошибка 1: Забыть вызвать родительский конструктор

Самая популярная ошибка. Если ты переопределяешь __construct() в дочернем классе, родительский конструктор не вызывается автоматически. Ты должен сделать это сам через parent::__construct().

<?php
class ParentClass {
    public function __construct() {
        echo "Родитель создан!
";
    }
}

class ChildClass extends ParentClass {
    public function __construct() {
        // Забыли вызвать parent::__construct()!
        echo "Ребёнок создан!
";
    }
}

$obj = new ChildClass();
// Выведет только: Ребёнок создан!
// "Родитель создан!" не выведется!
?>

Ошибка 2: Путаница между public, protected и private

Новички часто делают все свойства public «чтобы было проще». Это нарушает принцип инкапсуляции. Или, наоборот, ставят private и потом удивляются, что наследники не могут получить доступ к данным.

✅ Правило: Если свойство нужно только внутри класса — private. Если нужно внутри класса и его наследникам — protected. Если нужно всем — public (но старайся избегать).

Ошибка 3: Слишком глубокая иерархия наследования

Создание цепочек вида ClassA -> ClassB -> ClassC -> ClassD -> ClassE — это антипаттерн. Это называется «переусложнение» (overengineering). Такой код тяжело читать, тестировать и поддерживать.

💡 Совет: Старайся, чтобы глубина наследования не превышала 2-3 уровня. Если тебе нужно больше, возможно, стоит использовать композицию (включение одного класса в другой) вместо наследования.

Ошибка 4: Нарушение принципа подстановки Лисков (LSP)

Это сложное название простой идеи: объекты дочернего класса должны вести себя так, чтобы их можно было использовать везде, где используется родительский класс, не ломая программу. Если твой класс Пингвин наследует от класса Птица и переопределяет метод летать() так, что он выбрасывает исключение «Пингвины не летают», ты нарушаешь LSP. Лучше сделать интерфейс Летающий и не включать метод летать() в базовый класс Птица.

Ошибка 5: Использование наследования там, где нужна композиция

Классический пример: «У машины есть двигатель». Это композиция (has-a). Неправильно делать class Car extends Engine (наследование). Правильно: class Car { private $engine; }