Что такое полиморфизм
Полиморфизм — это принцип, при котором один и тот же интерфейс работает с разными типами данных. Вызывающий код не знает (и не должен знать), какой конкретный класс перед ним — он просто вызывает метод, а нужная реализация подставляется автоматически.
В 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, Cacheable | Exporter, 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.