Полиморфизм в PHP и Laravel
Glossary overview

Полиморфизм в PHP и Laravel

Что такое полиморфизм

Полиморфизм — это принцип, при котором один и тот же интерфейс работает с разными типами данных. Вызывающий код не знает (и не должен знать), какой конкретный класс перед ним — он просто вызывает метод, а нужная реализация подставляется автоматически.

В PHP полиморфизм реализуется несколькими способами:

  • через наследование,
  • интерфейсы,
  • абстрактные классы
  • morph-связи на уровне базы данных.

1. Наследование — переопределение поведения

Базовый класс задаёт метод, дочерние классы переопределяют его под свои нужды.

class Payment
{
    public function charge(float $amount): void
    {
        throw new \RuntimeException('Реализуй в дочернем классе');
    }
}

class CardPayment extends Payment
{
    public function charge(float $amount): void
    {
        // логика списания с карты
    }
}

class CryptoPayment extends Payment
{
    public function charge(float $amount): void
    {
        // логика списания крипты
    }
}

Вызывающему коду всё равно, какой тип оплаты — он работает с любым Payment:

function processOrder(Payment $payment, float $total): void
{
    $payment->charge($total); // какой класс — не важно
}

processOrder(new CardPayment(), 100);
processOrder(new CryptoPayment(), 100);

Ключевое слово — extends. Дочерний класс наследует всё от родителя и может переопределить нужные методы.

2. Интерфейсы — контракт без реализации

Интерфейс описывает что класс умеет делать, но не говорит как. Это контракт: «если ты реализуешь этот интерфейс, у тебя обязан быть такой-то метод».

interface Notifiable
{
    public function send(string $message): void;
}

class EmailNotification implements Notifiable
{
    public function send(string $message): void
    {
        // отправить email
    }
}

class TelegramNotification implements Notifiable
{
    public function send(string $message): void
    {
        // отправить в Telegram
    }
}

class SmsNotification implements Notifiable
{
    public function send(string $message): void
    {
        // отправить SMS
    }
}

Теперь любой код может принимать Notifiable и работать с чем угодно:

function alertUser(Notifiable $channel, string $text): void
{
    $channel->send($text); // email, telegram, sms — без разницы
}

Важное отличие от наследования: класс может реализовать несколько интерфейсов, но наследоваться можно только от одного класса.

class User implements Authenticatable, Notifiable, HasRoles
{
    // реализация всех трёх контрактов
}

3. Абстрактные классы — контракт плюс общая логика

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

abstract class Exporter
{
    // Общая логика — одинакова для всех
    public function export(Collection $data): string
    {
        $prepared = $this->prepare($data);
        return $this->format($prepared);
    }

    protected function prepare(Collection $data): array
    {
        return $data->toArray();
    }

    // Абстрактный метод — каждый реализует по-своему
    abstract protected function format(array $data): string;
}

class CsvExporter extends Exporter
{
    protected function format(array $data): string
    {
        // преобразование в CSV
    }
}

class JsonExporter extends Exporter
{
    protected function format(array $data): string
    {
        return json_encode($data, JSON_PRETTY_PRINT);
    }
}

class XmlExporter extends Exporter
{
    protected function format(array $data): string
    {
        // преобразование в XML
    }
}

Использование:

function generateReport(Exporter $exporter, Collection $data): string
{
    return $exporter->export($data);
    // CsvExporter → CSV, JsonExporter → JSON, XmlExporter → XML
}

Когда что выбрать

ИнтерфейсАбстрактный класс
Содержит кодНет, только сигнатуры методовДа, может содержать готовую логику
Множественное наследованиеДа, можно реализовать несколькоНет, только один родитель
Когда использоватьРазные классы, общий контрактПохожие классы, общая логика
ПримерNotifiable, CacheableExporter, BaseController

4. Полиморфизм в Laravel: драйверы

Laravel построен на полиморфизме. Практически все его компоненты работают через контракты (интерфейсы) из Illuminate\Contracts, а конкретная реализация подставляется через сервис-контейнер. Ты переключаешь драйвер в конфиге, а код не меняется.

Файловая система

// Один метод put() — разные хранилища
Storage::disk('local')->put('file.txt', 'данные');
Storage::disk('s3')->put('file.txt', 'данные');
Storage::disk('ftp')->put('file.txt', 'данные');

Под капотом — интерфейс Filesystem, три разных класса-драйвера. Переключение хранилища — одна строка в конфиге.

Кэш

Cache::store('redis')->get('key');
Cache::store('file')->get('key');
Cache::store('memcached')->get('key');

Тот же принцип: интерфейс Cache\Store, разные реализации.

Очереди

Queue::connection('redis')->push($job);
Queue::connection('sqs')->push($job);
Queue::connection('database')->push($job);

Уведомления

$user->notify(new OrderShipped($order));
// Laravel сам определяет, по каким каналам отправить:
// email, SMS, Telegram, Slack — всё через один метод notify()

Это и есть полиморфизм: один вызов ->put(), ->get(), ->push(), ->notify() — а что происходит внутри, зависит от конкретной реализации.

5. Morph-связи — полиморфизм в базе данных

Morph-связи решают задачу: одна таблица должна быть связана с несколькими разными моделями.

Пример: комментарии к постам и видео

Без полиморфизма пришлось бы делать отдельную таблицу комментариев для каждой сущности. С morph-связями — одна таблица:

comments:
  id
  body
  commentable_id    ← ID записи (поста или видео)
  commentable_type  ← имя модели ("App\Models\Post" или "App\Models\Video")

Laravel смотрит на commentable_type и подставляет нужную модель.

Модели

class Comment extends Model
{
    // "Я принадлежу чему-то — посту, видео, чему угодно"
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Использование

// Комментарии поста
$post->comments;

// Комментарии видео
$video->comments;

// От комментария — к родителю
$comment->commentable; // вернёт Post или Video

Типы morph-связей

СвязьМетодПример
Один ко многимmorphMany / morphToПост → много комментариев
Один к одномуmorphOne / morphToПользователь → одна аватарка
Многие ко многимmorphToMany / morphedByManyПост и видео → общие теги

Morph Map — читаемые алиасы

По умолчанию в базу записывается полное имя класса. Лучше задать короткие алиасы:

// AppServiceProvider::boot()
Relation::enforceMorphMap([
    'post'  => Post::class,
    'video' => Video::class,
]);

Теперь в commentable_type будет "post" вместо "App\Models\Post". Это чище и не сломается при переименовании класса.

Связь с static::class

Когда Laravel записывает commentable_type, он вызывает:

public function getMorphClass()
{
    return static::class;
}

Именно static::class гарантирует, что Post вернёт "App\Models\Post", а не "Illuminate\Database\Eloquent\Model".

6. Duck typing — неявный полиморфизм

PHP не строго типизирован, поэтому иногда полиморфизм работает «по факту» — если у объекта есть нужный метод, его можно вызвать без интерфейса:

class Duck
{
    public function quack(): string { return 'Кря'; }
}

class Person
{
    public function quack(): string { return 'Кря-кря (притворяюсь)'; }
}

function makeSound($thing): void
{
    echo $thing->quack(); // оба сработают
}

Это работает, но ненадёжно. В продакшн-коде лучше использовать интерфейсы — они дают проверку на этапе компиляции и понятный контракт.

Шпаргалка

МеханизмКлючевое словоКогда использовать
НаследованиеextendsОбщий родитель + переопределение поведения
ИнтерфейсimplementsКонтракт без реализации, несколько «ролей»
Абстрактный классabstractКонтракт + общая логика
Драйверы LaravelКонтракты + контейнерСменные реализации (кэш, очереди, диски)
Morph-связиmorphTo, morphManyОдна таблица → несколько моделей
Duck typingбез ключевых словБыстрые скрипты, не для продакшена

Итого

  • Полиморфизм — один интерфейс, разные реализации.
  • В PHP: наследование (extends), интерфейсы (implements), абстрактные классы (abstract).
  • В Laravel: драйверы (Storage, Cache, Queue) работают через контракты — переключение одной строкой в конфиге.
  • Morph-связи — полиморфизм на уровне базы данных: одна таблица связана с разными моделями через колонку _type.
  • Для надёжного кода используйте интерфейсы, а не duck typing.