Table of Contents
Классическая база – это книга “Design Patterns” от так называемой Gang of Four. Там 23 шаблона. Их делят на три группы: порождающие, структурные и поведенческие.

В реальной жизни 90% кода использует не все 23. Чаще всего встречаются:
- Factory
- Strategy
- Decorator
- Observer
- Adapter
- Template Method
Часто используешь — Factory, Builder, Decorator, Facade, Observer, Strategy, Repository.
Иногда — Proxy, Adapter, Command, Chain of Responsibility.
Порождающие шаблоны
Factory
Фабрика — это порождающий паттерн, который предоставляет интерфейс для создания объектов, скрывая логику их инстанцирования. Вместо new ClassName() ты вызываешь метод, который решает, какой именно класс создать.
Один класс создает другой.
Simple Factory (Простая фабрика)
Не канонический GoF-паттерн, но самый распространённый вариант на практике.
class PaymentFactory
{
public static function create(string $type): PaymentInterface
{
return match($type) {
'stripe' => new StripePayment(),
'paypal' => new PaypalPayment(),
'crypto' => new CryptoPayment(),
default => throw new \InvalidArgumentException("Unknown type: $type"),
};
}
}
// Использование
$payment = PaymentFactory::create('stripe');
$payment->charge(100);
Минус — при добавлении нового типа нужно менять сам класс фабрики (нарушает Open/Closed).
Factory Method (Фабричный метод)
Паттерн из GoF. Базовый класс определяет скелет алгоритма, а подклассы решают, какой объект создать.
abstract class Notifier
{
// Фабричный метод
abstract protected function createChannel(): ChannelInterface;
public function send(string $message): void
{
$channel = $this->createChannel();
$channel->deliver($message);
}
}
class EmailNotifier extends Notifier
{
protected function createChannel(): ChannelInterface
{
return new EmailChannel();
}
}
class SmsNotifier extends Notifier
{
protected function createChannel(): ChannelInterface
{
return new SmsChannel();
}
}
// Интерфейс — контракт, который должны выполнять все каналы
interface ChannelInterface
{
public function deliver(string $message): void;
}
// Конкретная реализация для email
class EmailChannel implements ChannelInterface
{
public function deliver(string $message): void
{
// Здесь реальная логика отправки письма
mail('[email protected]', 'Notification', $message);
// или через PHPMailer / Symfony Mailer / etc.
}
}
// Конкретная реализация для SMS
class SmsChannel implements ChannelInterface
{
public function deliver(string $message): void
{
// Здесь реальная логика отправки SMS
// Например, вызов Twilio API
}
}
Добавляешь новый канал — создаёшь новый подкласс, старый код не трогаешь.
Главная идея
Метод send() написан один раз в родителе и никогда не меняется. Вся разница — в том, какой канал вернёт createChannel(). Хочешь добавить Telegram — создаёшь TelegramChannel и TelegramNotifier, остальной код не трогаешь.
Как это работает в момент вызова
$notifier = new EmailNotifier();
$notifier->send("Ваш заказ отправлен");
```
Цепочка вызовов:
```
send("Ваш заказ отправлен")
→ $this->createChannel() // вызывается метод EmailNotifier
→ return new EmailChannel() // создаётся конкретный объект
→ $channel->deliver($message) // вызывается deliver() у EmailChannel
→ mail(...) // реальная отправка
Abstract Factory (Абстрактная фабрика)
Фабрика фабрик. Создаёт семейства взаимосвязанных объектов. То есть когда сущности делятся на подсущности. Например:
- Worker -> Developer -> PHP Developer / JS Developer
- Worker -> Manager -> Project Manager / Product Manager
interface UIFactory
{
public function createButton(): ButtonInterface;
public function createModal(): ModalInterface;
}
class DarkThemeFactory implements UIFactory
{
public function createButton(): ButtonInterface { return new DarkButton(); }
public function createModal(): ModalInterface { return new DarkModal(); }
}
class LightThemeFactory implements UIFactory
{
public function createButton(): ButtonInterface { return new LightButton(); }
public function createModal(): ModalInterface { return new LightModal(); }
}
// Вся UI гарантированно из одной темы
$factory = new DarkThemeFactory();
$button = $factory->createButton();
$modal = $factory->createModal();
В контексте Laravel / Filament
Laravel активно использует паттерн повсюду: DB::connection(), Storage::disk(), Cache::store() — всё это фабричные методы. В Filament при кастомизации компонентов ты по сути работаешь с похожим подходом, переопределяя методы создания форм, таблиц и т.д.
Static Factory
Это просто фабричный метод, объявленный как static. Не отдельный GoF-паттерн, но очень распространённый приём. Статический метод создаёт объект, скрывая детали инстанцирования.
// Интерфейс — контракт для всех работников
interface Worker
{
public function work(): void;
}
// Конкретные классы работников
class Developer implements Worker
{
public function work(): void
{
echo "Developer пишет код" . PHP_EOL;
}
}
class Designer implements Worker
{
public function work(): void
{
echo "Designer рисует макеты" . PHP_EOL;
}
}
class Manager implements Worker
{
public function work(): void
{
echo "Manager проводит митинги" . PHP_EOL;
}
}
// Фабрика
class WorkerFactory
{
public static function make(string $workerTitle): ?Worker
{
$className = ucfirst(strtolower($workerTitle)); // 'developer' → 'Developer'
if (class_exists($className)) {
return new $className();
}
return null;
}
}
// Использование
$developer = WorkerFactory::make('developer');
$developer->work(); // Developer пишет код
$designer = WorkerFactory::make('designer');
$designer->work(); // Designer рисует макеты
$manager = WorkerFactory::make('manager');
$manager->work(); // Manager проводит митинги
$unknown = WorkerFactory::make('tester');
var_dump($unknown); // NULL
Builder
Builder — порождающий паттерн, который позволяет создавать сложные объекты пошагово. Вместо конструктора с кучей параметров ты вызываешь цепочку методов и в конце получаешь готовый объект.
// Без Builder — конструктор с кучей параметров, легко запутаться
$user = new User('John', 'Doe', 25, '[email protected]', true, false, 'admin', 'Ukraine');
Непонятно что означает каждый true/false, порядок параметров легко перепутать.
Решение с Builder
// Класс-модель пользователя (то что в итоге строим)
class User
{
public function __construct(
public readonly string $firstName,
public readonly string $lastName,
public readonly int $age,
public readonly string $email,
public readonly string $role,
public readonly bool $isActive,
public readonly string $country,
) {}
}
// Строитель
class UserBuilder
{
private string $firstName = '';
private string $lastName = '';
private int $age = 0;
private string $email = '';
private string $role = 'user';
private bool $isActive = true;
private string $country = '';
public function firstName(string $value): static
{
$this->firstName = $value;
return $this; // возвращаем себя для цепочки
}
public function lastName(string $value): static
{
$this->lastName = $value;
return $this;
}
public function age(int $value): static
{
$this->age = $value;
return $this;
}
public function email(string $value): static
{
$this->email = $value;
return $this;
}
public function role(string $value): static
{
$this->role = $value;
return $this;
}
public function asAdmin(): static
{
$this->role = 'admin';
return $this;
}
public function inactive(): static
{
$this->isActive = false;
return $this;
}
public function country(string $value): static
{
$this->country = $value;
return $this;
}
// Финальный метод — собирает и возвращает объект
public function build(): User
{
return new User(
firstName: $this->firstName,
lastName: $this->lastName,
age: $this->age,
email: $this->email,
role: $this->role,
isActive: $this->isActive,
country: $this->country,
);
}
}
// Использование
$user = (new UserBuilder())
->firstName('John')
->lastName('Doe')
->age(25)
->email('[email protected]')
->country('Ukraine')
->asAdmin()
->build();
Builder создаёт объект пошагово и фокусируется на конфигурации одного сложного объекта.
В Laravel это повсюду
Laravel Query Builder — классический пример паттерна:
$users = DB::table('users')
->where('active', true)
->where('country', 'Ukraine')
->orderBy('created_at', 'desc')
->limit(10)
->get(); // аналог build()
Каждый метод возвращает $this, в конце get() собирает и выполняет запрос.
Prototype
Prototype — порождающий паттерн, который позволяет копировать объекты, не вдаваясь в детали их реализации. Вместо создания объекта с нуля — клонируешь уже существующий и меняешь только то что нужно.
Проблема которую решает
Представь объект с кучей полей, который дорого создавать с нуля (например, нужно сходить в БД, выполнить тяжёлые вычисления). Или просто нужно несколько похожих объектов с небольшими отличиями.
// Создаём с нуля каждый раз — дублирование и неудобно
$adminUser = new User('Admin', '[email protected]', 'admin', 'Ukraine');
$adminUser2 = new User('Admin2', '[email protected]', 'admin', 'Ukraine');
$adminUser3 = new User('Admin3', '[email protected]', 'admin', 'Ukraine');
Решение с Prototype
В PHP для клонирования есть встроенное ключевое слово clone, и магический метод __clone() который вызывается в момент клонирования.
class User
{
public function __construct(
public string $name,
public string $email,
public string $role,
public string $country,
public Address $address, // вложенный объект
) {}
// Вызывается автоматически при clone
public function __clone()
{
// Без этого $address будет ссылкой на тот же объект!
$this->address = clone $this->address;
}
// Удобный метод для цепочки
public function withName(string $name): static
{
$clone = clone $this;
$clone->name = $name;
return $clone;
}
public function withEmail(string $email): static
{
$clone = clone $this;
$clone->email = $email;
return $clone;
}
}
class Address
{
public function __construct(
public string $city,
public string $country,
) {}
}
// Создаём прототип один раз
$prototype = new User(
name: 'Template',
email: '',
role: 'admin',
country: 'Ukraine',
address: new Address('Kyiv', 'Ukraine'),
);
// Клонируем и меняем только нужные поля
$admin1 = (clone $prototype)->withName('John')->withEmail('[email protected]');
$admin2 = (clone $prototype)->withName('Jane')->withEmail('[email protected]');
$admin3 = (clone $prototype)->withName('Bob')->withEmail('[email protected]');
Или вот пример:
class User
{
public function __construct(
public string $name,
public string $email,
public string $role = 'user',
) {}
}
// Создаём прототип — шаблон обычного пользователя
$prototype = new User(name: '', email: '', role: 'user');
// Клонируем и меняем только нужные поля
$user1 = clone $prototype;
$user1->name = 'John';
$user1->email = '[email protected]';
$user2 = clone $prototype;
$user2->name = 'Jane';
$user2->email = '[email protected]';
var_dump($user1);
var_dump($user2);
```
Результат:
```
object(User) { name: 'John', email: '[email protected]', role: 'user' }
object(User) { name: 'Jane', email: '[email protected]', role: 'user' }
role: 'user' у обоих одинаковый — взят из прототипа. Меняли только name и email. Вот и вся суть — клонируй готовый шаблон вместо того чтобы создавать с нуля каждый раз.
Паттерн Object Pool (Пул объектов)
Pool — паттерн, который держит набор заранее созданных объектов и выдаёт их по запросу. После использования объект не уничтожается, а возвращается обратно в пул.
Проблема которую решает
Некоторые объекты дорого создавать — подключение к БД, к Redis, к стороннему API. Если создавать новое соединение на каждый запрос — это медленно и расточительно.
class DatabaseConnection
{
private string $id;
public function __construct()
{
$this->id = uniqid('conn_');
echo "Создано соединение {$this->id}" . PHP_EOL;
}
public function query(string $sql): string
{
return "Результат запроса '{$sql}' через {$this->id}";
}
}
class ConnectionPool
{
// всегда 2 массива в Pool
private array $available = []; // свободные соединения
private array $inUse = []; // занятые соединения
private int $maxSize;
public function __construct(int $maxSize = 3)
{
$this->maxSize = $maxSize;
}
// Взять соединение из пула
public function acquire(): DatabaseConnection
{
if (!empty($this->available)) {
// Берём уже существующее
$conn = array_pop($this->available);
$this->inUse[] = $conn;
echo "Выдано существующее соединение" . PHP_EOL;
return $conn;
}
if (count($this->inUse) < $this->maxSize) {
// Создаём новое если не достигли лимита
$conn = new DatabaseConnection();
$this->inUse[] = $conn;
return $conn;
}
throw new \RuntimeException('Все соединения заняты');
}
// Вернуть соединение обратно в пул
public function release(DatabaseConnection $conn): void
{
$key = array_search($conn, $this->inUse);
if ($key !== false) {
unset($this->inUse[$key]);
$this->available[] = $conn;
echo "Соединение возвращено в пул" . PHP_EOL;
}
}
public function stats(): void
{
echo "Свободно: " . count($this->available) .
", Занято: " . count($this->inUse) . PHP_EOL;
}
}
// Использование
$pool = new ConnectionPool(maxSize: 3);
$conn1 = $pool->acquire(); // Создано соединение conn_1
$conn2 = $pool->acquire(); // Создано соединение conn_2
echo $conn1->query('SELECT * FROM users') . PHP_EOL;
$pool->release($conn1); // Соединение возвращено в пул
$conn3 = $pool->acquire(); // Выдано существующее соединение (conn_1 переиспользован)
$pool->stats(); // Свободно: 0, Занято: 2
```
---
### Вывод в консоли
```
Создано соединение conn_1
Создано соединение conn_2
Результат запроса 'SELECT * FROM users' через conn_1
Соединение возвращено в пул
Выдано существующее соединение ← conn_1 переиспользован, не создан заново
Свободно: 0, Занято: 2
Пример с работниками.
class Worker
{
private string $id;
public function __construct()
{
$this->id = uniqid('worker_');
echo "Создан Worker {$this->id}" . PHP_EOL;
}
public function work(string $task): void
{
echo "Worker {$this->id} выполняет: {$task}" . PHP_EOL;
}
public function getId(): string
{
return $this->id;
}
}
class WorkerPool
{
// всегда 2 массива в Pool
private array $freeWorkers = [];
private array $busyWorkers = [];
public function getWorker(): Worker
{
if (count($this->freeWorkers) === 0) {
// Свободных нет — создаём нового
$worker = new Worker();
} else {
// Берём свободного из пула
$worker = array_pop($this->freeWorkers);
echo "Взят существующий Worker {$worker->getId()}" . PHP_EOL;
}
$this->busyWorkers[] = $worker;
return $worker;
}
public function releaseWorker(Worker $worker): void
{
$key = array_search($worker, $this->busyWorkers);
if ($key !== false) {
unset($this->busyWorkers[$key]);
$this->freeWorkers[] = $worker;
echo "Worker {$worker->getId()} возвращён в пул" . PHP_EOL;
}
}
public function stats(): void
{
echo "Свободно: " . count($this->freeWorkers) .
", Занято: " . count($this->busyWorkers) . PHP_EOL;
}
}
// Использование
$pool = new WorkerPool();
$pool->stats(); // Свободно: 0, Занято: 0
$worker1 = $pool->getWorker(); // Создан Worker worker_1
$worker2 = $pool->getWorker(); // Создан Worker worker_2
$worker1->work('копать траншею');
$worker2->work('класть кирпичи');
$pool->stats(); // Свободно: 0, Занято: 2
$pool->releaseWorker($worker1); // Worker worker_1 возвращён в пул
$pool->stats(); // Свободно: 1, Занято: 1
$worker3 = $pool->getWorker(); // Взят существующий Worker worker_1 (не создан заново!)
$worker3->work('красить забор');
$pool->stats(); // Свободно: 0, Занято: 2
```
Вывод:
```
Свободно: 0, Занято: 0
Создан Worker worker_1
Создан Worker worker_2
Worker worker_1 выполняет: копать траншею
Worker worker_2 выполняет: класть кирпичи
Свободно: 0, Занято: 2
Worker worker_1 возвращён в пул
Свободно: 1, Занято: 1
Взят существующий Worker worker_1 ← переиспользован!
Worker worker_1 выполняет: красить забор
Свободно: 0, Занято: 2
Отличие от Prototype
В Prototype объект копируется и живёт своей жизнью. В Pool объект берётся в аренду и возвращается обратно — один и тот же объект используется многократно.
Где используется в реальности
В PHP это чаще встречается на уровне инфраструктуры, а не в коде приложения. Laravel использует пул соединений через PDO, Redis клиент (predis) тоже управляет пулом соединений под капотом. В swoole и roadrunner пулы используются активно, так как там процессы живут долго между запросами.
Структурные шаблоны
Если порождающие паттерны отвечают на вопрос “как создать объект”, то структурные отвечают на вопрос “как объекты связать и организовать между собой”.
Они описывают как строить связи между классами и объектами, чтобы получить более гибкую и удобную структуру.
Dependency Injection (Внедрение зависимостей)
Суть простая: объект не создаёт свои зависимости сам, а получает их снаружи.
Проблема без DI
class OrderService
{
private EmailNotifier $notifier;
public function __construct()
{
// Сам создаёт зависимость — это плохо
$this->notifier = new EmailNotifier();
}
public function createOrder(): void
{
// логика создания заказа
$this->notifier->send('Заказ создан');
}
}
Проблемы:
- хочешь заменить
EmailNotifierнаSmsNotifier— придётся лезть внутрь класса - хочешь протестировать
OrderService— он всегда потянет за собой реальныйEmailNotifier - классы жёстко связаны между собой
Решение с DI
interface NotifierInterface
{
public function send(string $message): void;
}
class EmailNotifier implements NotifierInterface
{
public function send(string $message): void
{
echo "Email: {$message}" . PHP_EOL;
}
}
class SmsNotifier implements NotifierInterface
{
public function send(string $message): void
{
echo "SMS: {$message}" . PHP_EOL;
}
}
class OrderService
{
// Зависимость приходит снаружи
public function __construct(
private NotifierInterface $notifier
) {}
public function createOrder(): void
{
// логика создания заказа
$this->notifier->send('Заказ создан');
}
}
// Сами решаем что передать
$service = new OrderService(new EmailNotifier());
$service->createOrder(); // Email: Заказ создан
// Хотим SMS — просто меняем снаружи, OrderService не трогаем
$service = new OrderService(new SmsNotifier());
$service->createOrder(); // SMS: Заказ создан
Три способа внедрить зависимость
1. Через конструктор — самый распространённый, зависимость обязательна:
class OrderService
{
public function __construct(
private NotifierInterface $notifier
) {}
}
2. Через метод (setter) — зависимость опциональна:
class OrderService
{
private NotifierInterface $notifier;
public function setNotifier(NotifierInterface $notifier): void
{
$this->notifier = $notifier;
}
}
3. Через параметр метода — зависимость нужна только в одном месте:
class OrderService
{
public function createOrder(NotifierInterface $notifier): void
{
$notifier->send('Заказ создан');
}
}
DI Container
Когда зависимостей становится много — удобно использовать контейнер, который сам разруливает кто что получает:
class Container
{
private array $bindings = [];
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
public function make(string $abstract): mixed
{
if (isset($this->bindings[$abstract])) {
return ($this->bindings[$abstract])($this);
}
throw new \RuntimeException("No binding for {$abstract}");
}
}
$container = new Container();
$container->bind(NotifierInterface::class, fn() => new EmailNotifier());
$container->bind(OrderService::class, fn($c) => new OrderService(
$c->make(NotifierInterface::class)
));
// Контейнер сам собирает объект со всеми зависимостями
$service = $container->make(OrderService::class);
$service->createOrder(); // Email: Заказ создан
В Laravel это и есть Service Container — когда ты делаешь app(OrderService::class), Laravel сам смотрит конструктор, понимает что нужен NotifierInterface, находит привязку и всё собирает автоматически.
Registry
Registry — это глобальное хранилище объектов, доступное из любой точки приложения. Регистрируешь объект под ключом — получаешь его откуда угодно.
class Registry
{
private static array $storage = [];
// Сохранить
public static function set(string $key, mixed $value): void
{
self::$storage[$key] = $value;
}
// Получить
public static function get(string $key): mixed
{
if (!isset(self::$storage[$key])) {
throw new \RuntimeException("Ключ '{$key}' не найден в реестре");
}
return self::$storage[$key];
}
// Проверить существование
public static function has(string $key): bool
{
return isset(self::$storage[$key]);
}
// Удалить
public static function remove(string $key): void
{
unset(self::$storage[$key]);
}
}
Использование
// В начале приложения регистрируем всё что нужно
Registry::set('db', new DatabaseConnection());
Registry::set('config', ['debug' => true, 'env' => 'local']);
Registry::set('logger', new Logger());
// В любом месте кода получаем
$db = Registry::get('db');
$config = Registry::get('config');
$logger = Registry::get('logger');
Реальный пример — конфиг приложения
// bootstrap.php — инициализация приложения
Registry::set('config', [
'db_host' => 'localhost',
'db_name' => 'myapp',
'debug' => true,
]);
Registry::set('db', new PDO(
'mysql:host=localhost;dbname=myapp',
'root',
'password'
));
// UserService.php — используем в сервисе
class UserService
{
public function getUsers(): array
{
$db = Registry::get('db');
return $db->query('SELECT * FROM users')->fetchAll();
}
}
// OrderService.php — используем в другом сервисе
class OrderService
{
public function getOrders(): array
{
$db = Registry::get('db');
return $db->query('SELECT * FROM orders')->fetchAll();
}
}
Registry vs DI Container
Это главный вопрос — они похожи, но разные:
// Registry — ты сам идёшь за зависимостью
class OrderService
{
public function getOrders(): array
{
$db = Registry::get('db'); // активно тянешь из реестра
return $db->query('...');
}
}
// DI — зависимость приходит к тебе сама
class OrderService
{
public function __construct(
private DatabaseConnection $db // пассивно получаешь снаружи
) {}
}
Registry называют Service Locator — антипаттерном, потому что зависимости класса скрыты внутри, их не видно снаружи. С DI всё явно — смотришь на конструктор и сразу понимаешь что нужно классу.
Где встречается в реальности
В Laravel Registry по сути это app() — Service Container. Когда пишешь app('db') или App::make(UserService::class) — это и есть реестр под капотом, только умный — он ещё умеет сам разруливать зависимости.
Composite
Composite позволяет работать с одиночными объектами и группами объектов одинаково. Строится в виде дерева — есть листья (одиночные объекты) и ветки (группы).
Проблема без Composite
// Хотим посчитать зарплату отдела
// Но отдел может содержать как сотрудников так и под-отделы
$ceo = new Employee('CEO', 5000);
$developer = new Employee('Developer', 3000);
$designer = new Employee('Designer', 2000);
$department = new Department();
$department->addEmployee($developer);
$department->addEmployee($designer);
// А если отдел содержит другой отдел?
// Приходится писать разную логику для Employee и Department
Решение
// Общий интерфейс для всех — и для одного и для группы
interface WorkerInterface
{
public function getSalary(): int;
public function getName(): string;
public function info(int $depth = 0): void;
}
// Лист — одиночный сотрудник
class Employee implements WorkerInterface
{
public function __construct(
private string $name,
private int $salary,
) {}
public function getSalary(): int
{
return $this->salary;
}
public function getName(): string
{
return $this->name;
}
public function info(int $depth = 0): void
{
$indent = str_repeat(' ', $depth);
echo "{$indent}- {$this->name} ({$this->salary}$)" . PHP_EOL;
}
}
// Ветка — отдел, может содержать сотрудников и другие отделы
class Department implements WorkerInterface
{
private array $members = [];
public function __construct(
private string $name
) {}
public function add(WorkerInterface $worker): void
{
$this->members[] = $worker;
}
public function getName(): string
{
return $this->name;
}
// Считает зарплату всех вложенных сотрудников рекурсивно
public function getSalary(): int
{
$total = 0;
foreach ($this->members as $member) {
$total += $member->getSalary(); // одинаково для Employee и Department
}
return $total;
}
public function info(int $depth = 0): void
{
$indent = str_repeat(' ', $depth);
echo "{$indent}[{$this->name}] итого: {$this->getSalary()}$" . PHP_EOL;
foreach ($this->members as $member) {
$member->info($depth + 1);
}
}
}
// Строим дерево
$cto = new Employee('CTO', 5000);
$developer = new Employee('Developer', 3000);
$designer = new Employee('Designer', 2000);
$tester = new Employee('Tester', 1500);
$devDepartment = new Department('Dev отдел');
$devDepartment->add($developer);
$devDepartment->add($designer);
$devDepartment->add($tester);
$company = new Department('Компания');
$company->add($cto);
$company->add($devDepartment); // добавляем целый отдел как один объект
$company->info();
echo "Общий бюджет: " . $company->getSalary() . "$" . PHP_EOL;
```
---
### Вывод
```
[Компания] итого: 11800$
- CTO (5000$)
[Dev отдел] итого: 6800$
- Developer (3000$)
- Designer (2000$)
- Tester (1500$)
Общий бюджет: 11800$
Главная идея
$company->getSalary() и $devDepartment->getSalary() и $developer->getSalary() — вызываются одинаково, хотя за ними стоит разная логика. Клиентскому коду не важно, один это объект или целое дерево.
Где встречается в реальности
Файловая система — файл и папка ведут себя одинаково (у обоих есть имя, размер). HTML DOM — один тег и группа тегов. В Laravel Filament — Forms\Components\Group который содержит другие компоненты, это и есть Composite.
Adapter
Adapter — позволяет объектам с несовместимыми интерфейсами работать вместе. Как переходник для розетки — европейская вилка не лезет в американскую розетку, но с адаптером всё работает.
Проблема
// Старый класс который нельзя менять (например сторонняя библиотека)
class OldEmailService
{
public function sendEmail(string $to, string $subject, string $body): void
{
echo "Отправка через старый сервис: {$to}" . PHP_EOL;
}
}
// Новый интерфейс который использует всё приложение
interface NotifierInterface
{
public function send(string $message, string $recipient): void;
}
// Хотим использовать OldEmailService через NotifierInterface
// Но у них разные методы — не совместимы напрямую
Решение — Adapter
// Адаптер оборачивает старый класс и реализует новый интерфейс
class EmailServiceAdapter implements NotifierInterface
{
public function __construct(
private OldEmailService $oldService
) {}
public function send(string $message, string $recipient): void
{
// Внутри переводим вызов нового интерфейса в вызов старого
$this->oldService->sendEmail(
to: $recipient,
subject: 'Notification',
body: $message,
);
}
}
// Использование — код работает через единый интерфейс
$adapter = new EmailServiceAdapter(new OldEmailService());
$adapter->send('Заказ создан', '[email protected]');
// Отправка через старый сервис: [email protected]
Более реальный пример — подключение разных платёжных систем
// Единый интерфейс для всех платёжек
interface PaymentInterface
{
public function charge(int $amount, string $currency): bool;
}
// Stripe со своим API
class StripeApi
{
public function createCharge(array $params): array
{
echo "Stripe: списываем {$params['amount']} {$params['currency']}" . PHP_EOL;
return ['status' => 'success'];
}
}
// PayPal со своим API
class PayPalApi
{
public function makePayment(int $cents, string $currencyCode): bool
{
echo "PayPal: списываем {$cents} {$currencyCode}" . PHP_EOL;
return true;
}
}
// Адаптер для Stripe
class StripeAdapter implements PaymentInterface
{
public function __construct(
private StripeApi $stripe
) {}
public function charge(int $amount, string $currency): bool
{
$result = $this->stripe->createCharge([
'amount' => $amount,
'currency' => $currency,
]);
return $result['status'] === 'success';
}
}
// Адаптер для PayPal
class PayPalAdapter implements PaymentInterface
{
public function __construct(
private PayPalApi $paypal
) {}
public function charge(int $amount, string $currency): bool
{
return $this->paypal->makePayment($amount, $currency);
}
}
// OrderService работает через единый интерфейс — ему всё равно Stripe или PayPal
class OrderService
{
public function __construct(
private PaymentInterface $payment
) {}
public function checkout(int $amount): void
{
$success = $this->payment->charge($amount, 'USD');
echo $success ? 'Оплата прошла' : 'Ошибка оплаты';
echo PHP_EOL;
}
}
// Используем Stripe
$service = new OrderService(new StripeAdapter(new StripeApi()));
$service->checkout(100);
// Stripe: списываем 100 USD
// Оплата прошла
// Переключаемся на PayPal — OrderService не трогаем
$service = new OrderService(new PayPalAdapter(new PayPalApi()));
$service->checkout(100);
// PayPal: списываем 100 USD
// Оплата прошла
Отличие от Decorator
Внешне похожи — оба оборачивают объект. Но цель разная:
Adapter — меняет интерфейс, чтобы несовместимые классы могли работать вместе. Решает проблему совместимости.
Decorator — оставляет интерфейс тем же, но добавляет новое поведение. Решает проблему расширения функциональности.
Где встречается в реальности
В Laravel Cache — один интерфейс, а под капотом адаптеры для Redis, Memcached, файловой системы. Storage — один интерфейс, адаптеры для local, S3, FTP. По сути каждый драйвер в Laravel это адаптер.
Паттерн Bridge (Мост)
Bridge — разделяет класс на две независимые иерархии: абстракцию и реализацию, чтобы они могли меняться независимо друг от друга.
Проблема без Bridge
Представь что нужно сделать уведомления для разных типов событий через разные каналы:
EmailOrderNotifier
EmailUserNotifier
SmsOrderNotifier
SmsUserNotifier
TelegramOrderNotifier
TelegramUserNotifier
Добавляешь новый канал — создаёшь 2 класса. Добавляешь новый тип события — создаёшь 3 класса. Комбинации множатся — это взрыв классов.
Решение
Разделяем на две независимые иерархии и соединяем мостом:
// РЕАЛИЗАЦИЯ — как отправить (канал)
interface MessageSender
{
public function sendMessage(string $title, string $body): void;
}
class EmailSender implements MessageSender
{
public function sendMessage(string $title, string $body): void
{
echo "Email | {$title}: {$body}" . PHP_EOL;
}
}
class SmsSender implements MessageSender
{
public function sendMessage(string $title, string $body): void
{
echo "SMS | {$title}: {$body}" . PHP_EOL;
}
}
class TelegramSender implements MessageSender
{
public function sendMessage(string $title, string $body): void
{
echo "Telegram | {$title}: {$body}" . PHP_EOL;
}
}
// АБСТРАКЦИЯ — что отправить (тип уведомления)
// Получает реализацию через конструктор — это и есть мост
abstract class Notifier
{
public function __construct(
protected MessageSender $sender // мост к реализации
) {}
abstract public function notify(array $data): void;
}
class OrderNotifier extends Notifier
{
public function notify(array $data): void
{
$this->sender->sendMessage(
title: 'Новый заказ',
body: "Заказ #{$data['id']} на сумму {$data['amount']}$",
);
}
}
class UserNotifier extends Notifier
{
public function notify(array $data): void
{
$this->sender->sendMessage(
title: 'Новый пользователь',
body: "Зарегистрирован {$data['name']} ({$data['email']})",
);
}
}
// Использование — комбинируем как хотим
$orderViaSms = new OrderNotifier(new SmsSender());
$orderViaEmail = new OrderNotifier(new EmailSender());
$orderViaTelegram = new OrderNotifier(new TelegramSender());
$userViaSms = new UserNotifier(new SmsSender());
$orderViaSms->notify(['id' => 42, 'amount' => 500]);
// SMS | Новый заказ: Заказ #42 на сумму 500$
$orderViaEmail->notify(['id' => 42, 'amount' => 500]);
// Email | Новый заказ: Заказ #42 на сумму 500$
$userViaSms->notify(['name' => 'John', 'email' => '[email protected]']);
// SMS | Новый пользователь: Зарегистрирован John ([email protected])
Добавляем новый канал — трогаем только одну сторону
// Просто добавляем новую реализацию
class PushNotificationSender implements MessageSender
{
public function sendMessage(string $title, string $body): void
{
echo "Push | {$title}: {$body}" . PHP_EOL;
}
}
// И сразу работает со всеми типами уведомлений
$orderViaPush = new OrderNotifier(new PushNotificationSender());
$userViaPush = new UserNotifier(new PushNotificationSender());
Где встречается в реальности
В Laravel драйверы — Queue может работать через Redis, SQS, database. Mail через SMTP, Mailgun, SES. Абстракция (Queue, Mail) и реализация (драйверы) меняются независимо — это и есть Bridge.
Паттерн Data Mapper
Data Mapper — разделяет объекты бизнес-логики и источник данных. Объект ничего не знает откуда пришли данные, а отдельный класс-маппер занимается преобразованием данных из любого источника (БД, API, XML, CSV) в объект и обратно.
Проблема без Data Mapper
// Active Record подход — объект сам знает как себя сохранить
class User
{
public function save(): void
{
// бизнес-объект знает про БД — это плохо
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)')
->execute([$this->name, $this->email]);
}
}
Объект привязан к БД — сложно тестировать, сложно менять хранилище.
Решение
// Чистый бизнес-объект — ничего не знает про БД
class User
{
public function __construct(
public readonly ?int $id,
public string $name,
public string $email,
public string $role = 'user',
) {}
// Только бизнес-логика
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function changeName(string $name): void
{
if (empty($name)) {
throw new \InvalidArgumentException('Имя не может быть пустым');
}
$this->name = $name;
}
}
// Маппер — знает про БД, но не знает про бизнес-логику
class UserMapper
{
public function __construct(
private PDO $pdo
) {}
// Найти по id
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
return $this->hydrate($row); // из массива в объект
}
// Найти всех
public function findAll(): array
{
$stmt = $this->pdo->query('SELECT * FROM users');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_map(fn($row) => $this->hydrate($row), $rows);
}
// Сохранить (insert или update)
public function save(User $user): User
{
if ($user->id === null) {
return $this->insert($user);
}
return $this->update($user);
}
// Удалить
public function delete(User $user): void
{
$this->pdo->prepare('DELETE FROM users WHERE id = ?')
->execute([$user->id]);
}
// Из массива БД в объект
private function hydrate(array $row): User
{
return new User(
id: (int) $row['id'],
name: $row['name'],
email: $row['email'],
role: $row['role'],
);
}
// Из объекта в массив для БД
private function extract(User $user): array
{
return [
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
];
}
private function insert(User $user): User
{
$stmt = $this->pdo->prepare(
'INSERT INTO users (name, email, role) VALUES (:name, :email, :role)'
);
$stmt->execute($this->extract($user));
return new User(
id: (int) $this->pdo->lastInsertId(),
name: $user->name,
email: $user->email,
role: $user->role,
);
}
private function update(User $user): User
{
$stmt = $this->pdo->prepare(
'UPDATE users SET name = :name, email = :email, role = :role WHERE id = :id'
);
$stmt->execute([...$this->extract($user), 'id' => $user->id]);
return $user;
}
}
// Использование
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$mapper = new UserMapper($pdo);
// Создаём и сохраняем
$user = new User(id: null, name: 'John', email: '[email protected]');
$user = $mapper->save($user);
echo $user->id . PHP_EOL; // 1
// Загружаем и меняем
$user = $mapper->findById(1);
$user->changeName('John Doe');
$mapper->save($user);
// Бизнес-логика без БД
echo $user->isAdmin() ? 'Админ' : 'Юзер'; // Юзер
Hydrate и Extract
Два ключевых метода маппера:
hydrate — заполняет объект данными из БД (массив → объект). Название от слова “гидратация” — наполнение.
extract — извлекает данные из объекта для записи в БД (объект → массив).
Data Mapper vs Active Record
// Active Record — объект сам работает с БД (как в Laravel Eloquent)
$user = User::find(1);
$user->name = 'John';
$user->save(); // объект знает про БД
// Data Mapper — объект чистый, маппер отдельно
$user = $mapper->findById(1);
$user->name = 'John';
$mapper->save($user); // маппер знает про БД
Active Record проще и быстрее писать — поэтому Laravel использует его в Eloquent. Data Mapper сложнее, но объекты чище и легче тестировать — поэтому Doctrine использует Data Mapper.
Паттерн Decorator (Декоратор)
Decorator — динамически добавляет объекту новое поведение, оборачивая его в другой объект. Альтернатива наследованию — вместо того чтобы создавать подклассы, оборачиваешь объект в декоратор.
Проблема без Decorator
// Хотим добавить логирование, кэширование, проверку прав
// Через наследование получается взрыв классов:
class UserService {}
class LoggedUserService extends UserService {}
class CachedUserService extends UserService {}
class CachedLoggedUserService extends UserService {}
class AuthCachedLoggedUserService extends UserService {}
// и так далее...
Решение
// Базовый интерфейс
interface UserServiceInterface
{
public function getUser(int $id): array;
}
// Основная реализация
class UserService implements UserServiceInterface
{
public function getUser(int $id): array
{
echo "Запрос в БД для user {$id}" . PHP_EOL;
return ['id' => $id, 'name' => 'John', 'email' => '[email protected]'];
}
}
// Базовый декоратор — оборачивает любой UserServiceInterface
abstract class UserServiceDecorator implements UserServiceInterface
{
public function __construct(
protected UserServiceInterface $service
) {}
}
// Декоратор логирования
class LoggingDecorator extends UserServiceDecorator
{
public function getUser(int $id): array
{
echo "LOG: запрос getUser({$id})" . PHP_EOL;
$result = $this->service->getUser($id);
echo "LOG: получен результат для user {$id}" . PHP_EOL;
return $result;
}
}
// Декоратор кэширования
class CacheDecorator extends UserServiceDecorator
{
private array $cache = [];
public function getUser(int $id): array
{
if (isset($this->cache[$id])) {
echo "CACHE: возвращаем из кэша user {$id}" . PHP_EOL;
return $this->cache[$id];
}
$result = $this->service->getUser($id);
$this->cache[$id] = $result;
return $result;
}
}
// Декоратор проверки прав
class AuthDecorator extends UserServiceDecorator
{
private bool $isAuthenticated;
public function __construct(UserServiceInterface $service, bool $isAuthenticated)
{
parent::__construct($service);
$this->isAuthenticated = $isAuthenticated;
}
public function getUser(int $id): array
{
if (!$this->isAuthenticated) {
throw new \RuntimeException('Доступ запрещён');
}
return $this->service->getUser($id);
}
}
// Оборачиваем как матрёшку — порядок важен
$service = new UserService();
$service = new LoggingDecorator($service);
$service = new CacheDecorator($service);
$service = new AuthDecorator($service, isAuthenticated: true);
$user = $service->getUser(1);
// LOG: запрос getUser(1)
// Запрос в БД для user 1
// LOG: получен результат для user 1
$user = $service->getUser(1);
// CACHE: возвращаем из кэша user 1
Каждый декоратор переопределяет метод getUser(), но внутри вызывает $this->service->getUser() — то есть передаёт вызов дальше по цепочке, добавляя своё поведение до или после.
AuthDecorator->getUser(1)
→ проверяет права
→ CacheDecorator->getUser(1)
→ проверяет кэш
→ LoggingDecorator->getUser(1)
→ пишет лог "запрос"
→ UserService->getUser(1)
→ идёт в БД
→ пишет лог "получен результат"
→ сохраняет в кэш
→ возвращает результат
Каждый слой делает своё дело и передаёт вызов вглубь. Это и есть матрёшка — один внутри другого. Без $this->service->getUser($id) цепочка бы оборвалась и до реального UserService вызов бы не дошёл.
Комбинируем как хотим
// Только логирование
$service = new LoggingDecorator(new UserService());
// Только кэш
$service = new CacheDecorator(new UserService());
// Кэш + логирование
$service = new LoggingDecorator(
new CacheDecorator(
new UserService()
)
);
Добавляешь новое поведение — создаёшь новый декоратор, существующий код не трогаешь.
Более простой пример
interface Coffee
{
public function cost(): int;
public function description(): string;
}
// Простой кофе
class SimpleCoffee implements Coffee
{
public function cost(): int
{
return 10;
}
public function description(): string
{
return 'Кофе';
}
}
// Декоратор молока
class MilkDecorator implements Coffee
{
public function __construct(
private Coffee $coffee
) {}
public function cost(): int
{
return $this->coffee->cost() + 5;
}
public function description(): string
{
return $this->coffee->description() . ' + молоко';
}
}
// Декоратор сахара
class SugarDecorator implements Coffee
{
public function __construct(
private Coffee $coffee
) {}
public function cost(): int
{
return $this->coffee->cost() + 2;
}
public function description(): string
{
return $this->coffee->description() . ' + сахар';
}
}
// Использование
$coffee = new SimpleCoffee();
echo $coffee->description() . ' — ' . $coffee->cost() . '$' . PHP_EOL;
// Кофе — 10$
$coffee = new MilkDecorator($coffee);
echo $coffee->description() . ' — ' . $coffee->cost() . '$' . PHP_EOL;
// Кофе + молоко — 15$
$coffee = new SugarDecorator($coffee);
echo $coffee->description() . ' — ' . $coffee->cost() . '$' . PHP_EOL;
// Кофе + молоко + сахар — 17$
Каждый декоратор добавляет свою цену и дописывает описание. Можно комбинировать как угодно — два молока, три сахара, всё что угодно.
Можем улучшить этот код. Сейчас мы в каждом декораторе вызываем конструктор, можно это вынест в базовый декоратор и делать это там.
interface Coffee
{
public function cost(): int;
public function description(): string;
}
class SimpleCoffee implements Coffee
{
public function cost(): int
{
return 10;
}
public function description(): string
{
return 'Кофе';
}
}
// Базовый декоратор — конструктор один раз
abstract class CoffeeDecorator implements Coffee
{
public function __construct(
protected Coffee $coffee
) {}
}
// Декораторы больше не дублируют конструктор
class MilkDecorator extends CoffeeDecorator
{
public function cost(): int
{
return $this->coffee->cost() + 5;
}
public function description(): string
{
return $this->coffee->description() . ' + молоко';
}
}
class SugarDecorator extends CoffeeDecorator
{
public function cost(): int
{
return $this->coffee->cost() + 2;
}
public function description(): string
{
return $this->coffee->description() . ' + сахар';
}
}
class VanillaDecorator extends CoffeeDecorator
{
public function cost(): int
{
return $this->coffee->cost() + 3;
}
public function description(): string
{
return $this->coffee->description() . ' + ваниль';
}
}
Теперь при добавлении нового декоратора пишешь только бизнес-логику — cost() и description(). Конструктор достаётся бесплатно от CoffeeDecorator.
Какой смысл в implements тут abstract class CoffeeDecorator implements Coffee, если мы в abstract class CoffeeDecorator не имплементируем методы Coffee
Смысл в том что implements Coffee гарантирует контракт — любой класс который extends CoffeeDecorator обязан реализовать cost() и description().
abstract class CoffeeDecorator implements Coffee
{
public function __construct(
protected Coffee $coffee
) {}
// cost() и description() не реализованы здесь
// но обязаны быть в каждом наследнике
}
// Если забудешь написать cost() — PHP выдаст ошибку
class MilkDecorator extends CoffeeDecorator
{
public function cost(): int // обязан быть
{
return $this->coffee->cost() + 5;
}
public function description(): string // обязан быть
{
return $this->coffee->description() . ' + молоко';
}
}
Отличие от Adapter и Bridge
Adapter — меняет интерфейс объекта чтобы он стал совместимым.
Bridge — разделяет две иерархии чтобы они развивались независимо.
Decorator — оставляет интерфейс тем же, но добавляет новое поведение.
Где встречается в реальности
В Laravel middleware — это классический Decorator. Каждый middleware оборачивает следующий и добавляет поведение (логирование, авторизация, throttling) не меняя основной обработчик запроса. PHP streams тоже используют этот паттерн — gzip, encryption оборачивают базовый поток.
Паттерн Facade (Фасад)
Facade — предоставляет простой интерфейс к сложной системе из множества классов. Скрывает сложность за одним простым классом.
Проблема без Facade
// Чтобы оформить заказ нужно вручную дёргать кучу классов
$cart = new Cart();
$cart->add($product);
$payment = new PaymentService();
$payment->charge($amount);
$inventory = new InventoryService();
$inventory->decrease($product);
$delivery = new DeliveryService();
$delivery->schedule($order);
$email = new EmailService();
$email->sendConfirmation($user);
$logger = new Logger();
$logger->log('Order created');
Каждый раз когда оформляешь заказ — повторяешь все эти шаги. Клиентский код знает про все внутренние классы.
Решение с Facade
// Сложные подсистемы
class CartService
{
public function add(int $productId): void
{
echo "Товар {$productId} добавлен в корзину" . PHP_EOL;
}
public function clear(): void
{
echo "Корзина очищена" . PHP_EOL;
}
}
class PaymentService
{
public function charge(int $amount): bool
{
echo "Списано {$amount}$" . PHP_EOL;
return true;
}
}
class InventoryService
{
public function decrease(int $productId): void
{
echo "Остаток товара {$productId} уменьшен" . PHP_EOL;
}
}
class DeliveryService
{
public function schedule(string $address): void
{
echo "Доставка запланирована на {$address}" . PHP_EOL;
}
}
class EmailService
{
public function sendConfirmation(string $email): void
{
echo "Письмо отправлено на {$email}" . PHP_EOL;
}
}
// Фасад — один простой класс для всей этой кухни
class OrderFacade
{
public function __construct(
private CartService $cart,
private PaymentService $payment,
private InventoryService $inventory,
private DeliveryService $delivery,
private EmailService $email,
) {}
public function placeOrder(
int $productId,
int $amount,
string $address,
string $email
): bool {
$this->cart->add($productId);
$paid = $this->payment->charge($amount);
if (!$paid) {
echo "Ошибка оплаты" . PHP_EOL;
return false;
}
$this->inventory->decrease($productId);
$this->delivery->schedule($address);
$this->email->sendConfirmation($email);
$this->cart->clear();
echo "Заказ оформлен!" . PHP_EOL;
return true;
}
}
// Использование — один вызов вместо шести
$facade = new OrderFacade(
new CartService(),
new PaymentService(),
new InventoryService(),
new DeliveryService(),
new EmailService(),
);
$facade->placeOrder(
productId: 42,
amount: 100,
address: 'Kyiv, Ukraine',
email: '[email protected]'
);
```
---
### Вывод
```
Товар 42 добавлен в корзину
Списано 100$
Остаток товара 42 уменьшен
Доставка запланирована на Kyiv, Ukraine
Письмо отправлено на [email protected]
Корзина очищена
Заказ оформлен!
Где встречается в реальности
В Laravel Auth::login(), Mail::send(), Storage::put() — всё это фасады. За Mail::send() скрывается целая система — SMTP соединение, очередь, логирование, шаблонизатор. Но ты вызываешь один метод и не думаешь об этом.
Fluent Interface (Текучий интерфейс)
Fluent Interface — это не GoF-паттерн, но очень популярный приём. Суть в том что каждый метод возвращает $this — это позволяет вызывать методы цепочкой.
Без Fluent Interface
$query = new QueryBuilder();
$query->select('name, email');
$query->from('users');
$query->where('active = 1');
$query->orderBy('name');
$query->limit(10);
$result = $query->get();
С Fluent Interface
class QueryBuilder
{
private string $table = '';
private string $fields = '*';
private array $wheres = [];
private string $order = '';
private ?int $limit = null;
public function select(string $fields): static
{
$this->fields = $fields;
return $this; // возвращаем себя
}
public function from(string $table): static
{
$this->table = $table;
return $this;
}
public function where(string $condition): static
{
$this->wheres[] = $condition;
return $this;
}
public function orderBy(string $field): static
{
$this->order = $field;
return $this;
}
public function limit(int $limit): static
{
$this->limit = $limit;
return $this;
}
public function get(): string
{
$sql = "SELECT {$this->fields} FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->order) {
$sql .= " ORDER BY {$this->order}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
}
// Использование — читается как предложение
$sql = (new QueryBuilder())
->select('name, email')
->from('users')
->where('active = 1')
->where('age > 18')
->orderBy('name')
->limit(10)
->get();
echo $sql;
// SELECT name, email FROM users WHERE active = 1 AND age > 18 ORDER BY name LIMIT 10
static vs this как тайп-хинт
// self — всегда возвращает THIS класс
public function where(string $condition): self {}
// static — возвращает тот класс который вызвал метод (позднее связывание)
public function where(string $condition): static {}
static лучше когда планируешь наследование:
class BaseBuilder
{
public function where(string $condition): static
{
$this->wheres[] = $condition;
return $this;
}
}
class UserBuilder extends BaseBuilder
{
public function active(): static
{
return $this->where('active = 1');
}
}
$builder = new UserBuilder();
$builder->active()->where('age > 18'); // работает — возвращает UserBuilder, а не BaseBuilder
С self последняя строка вернула бы BaseBuilder и метод active() был бы недоступен.
Где встречается в реальност
Везде в Laravel — это основа всего:
// Eloquent
User::where('active', 1)
->where('role', 'admin')
->orderBy('name')
->limit(10)
->get();
// Filament Forms
Forms\Components\TextInput::make('name')
->label('Имя')
->required()
->maxLength(255)
->placeholder('Введите имя');
// Builder паттерн тоже использует Fluent Interface
(new UserBuilder())
->firstName('John')
->lastName('Doe')
->asAdmin()
->build();
Fluent Interface — это не самостоятельный паттерн а скорее техника, которая делает Builder, QueryBuilder и другие паттерны удобными в использовании.
Паттерн Flyweight (Легковес)
Flyweight — экономит память, переиспользуя общие данные между множеством похожих объектов. Вместо того чтобы хранить одинаковые данные в каждом объекте — выносишь их в одно место и делишься ими.
Идея
Данные объекта делятся на две части:
Внутреннее состояние (intrinsic) — одинаковое для многих объектов, хранится в Flyweight и переиспользуется.
Внешнее состояние (extrinsic) — уникальное для каждого объекта, передаётся снаружи при вызове.
Проблема без Flyweight
Представь интернет-магазин где тысячи заказов. В каждой позиции заказа хранится информация о товаре:
class OrderItem
{
public function __construct(
private int $orderId,
private int $quantity,
private float $price,
private string $name, // дублируется
private string $description, // дублируется
private string $image, // дублируется
) {}
}
// 1000 заказов по 3 товара = 3000 объектов
// В каждом хранится одно и то же название, описание, картинка
$items[] = new OrderItem(1, 2, 999.0, 'iPhone 15', 'Смартфон Apple', 'iphone.jpg');
$items[] = new OrderItem(2, 1, 999.0, 'iPhone 15', 'Смартфон Apple', 'iphone.jpg');
$items[] = new OrderItem(3, 3, 999.0, 'iPhone 15', 'Смартфон Apple', 'iphone.jpg');
// iPhone 15 повторяется 1000 раз в памяти — хотя это один и тот же товар
Решение с Flyweight
// Flyweight — общие данные товара (одинаковы для всех экземпляров)
class ProductType
{
public function __construct(
private string $name,
private string $description,
private string $image,
) {}
public function display(int $orderId, int $quantity, float $price): void
{
echo "Заказ #{$orderId} | {$this->name} | {$quantity}шт | {$price}$ | фото: {$this->image}" . PHP_EOL;
}
}
// Фабрика — не создаёт дубли
class ProductTypeFactory
{
private static array $types = [];
public static function get(string $name, string $description, string $image): ProductType
{
if (!isset(self::$types[$name])) {
self::$types[$name] = new ProductType($name, $description, $image);
}
return self::$types[$name];
}
public static function count(): int
{
return count(self::$types);
}
}
// Позиция заказа — хранит только уникальные данные
class OrderItem
{
private ProductType $productType;
public function __construct(
private int $orderId,
private int $quantity,
private float $price,
string $name,
string $description,
string $image,
) {
$this->productType = ProductTypeFactory::get($name, $description, $image);
}
public function display(): void
{
$this->productType->display($this->orderId, $this->quantity, $this->price);
}
}
// 1000 заказов содержат одни и те же товары
$items = [];
for ($i = 1; $i <= 5; $i++) {
$items[] = new OrderItem($i, 2, 999.0, 'iPhone 15', 'Смартфон Apple', 'iphone.jpg');
$items[] = new OrderItem($i, 1, 1299.0, 'MacBook', 'Ноутбук Apple', 'macbook.jpg');
$items[] = new OrderItem($i, 3, 29.0, 'Чехол', 'Чехол для iPhone', 'case.jpg');
}
foreach ($items as $item) {
$item->display();
}
echo PHP_EOL;
echo "Позиций заказов: " . count($items) . PHP_EOL; // 15
echo "ProductType в памяти: " . ProductTypeFactory::count() . PHP_EOL; // 3
```
Вывод:
```
Заказ #1 | iPhone 15 | 2шт | 999$ | фото: iphone.jpg
Заказ #1 | MacBook | 1шт | 1299$ | фото: macbook.jpg
Заказ #1 | Чехол | 3шт | 29$ | фото: case.jpg
...
Позиций заказов: 15
ProductType в памяти: 3 ← название, описание, картинка хранятся 3 раза, не 15
15 позиций заказов, но ProductType с тяжёлыми данными (описание, картинка) — только 3 объекта. Чем больше заказов, тем очевиднее экономия.
Паттерн Proxy (Заместитель)
Proxy — подставной объект, который стоит перед реальным объектом и контролирует доступ к нему. Клиент думает что работает с реальным объектом, но на самом деле работает с прокси.
Три основных вида Proxy
- Virtual Proxy — откладывает создание тяжёлого объекта до момента когда он реально нужен (ленивая загрузка).
- Protection Proxy — контролирует доступ к объекту (проверка прав).
- Caching Proxy — кэширует результаты вызовов.
Virtual Proxy — ленивая загрузка
interface ImageInterface
{
public function display(): void;
}
// Реальный объект — тяжёлый, грузит файл с диска
class RealImage implements ImageInterface
{
private string $data;
public function __construct(private string $filename)
{
$this->load();
}
private function load(): void
{
echo "Загрузка файла {$this->filename} с диска..." . PHP_EOL;
$this->data = "данные_{$this->filename}";
}
public function display(): void
{
echo "Отображаю {$this->filename}" . PHP_EOL;
}
}
// Прокси — создаёт RealImage только когда реально нужен
class ImageProxy implements ImageInterface
{
private ?RealImage $image = null;
public function __construct(private string $filename) {}
public function display(): void
{
if ($this->image === null) {
$this->image = new RealImage($this->filename); // создаём только здесь
}
$this->image->display();
}
}
// Использование
$image = new ImageProxy('photo.jpg');
// Файл ещё не загружен!
echo "Делаем что-то другое..." . PHP_EOL;
$image->display(); // только сейчас загружается файл
$image->display(); // файл уже загружен — переиспользуем
```
Вывод:
```
Делаем что-то другое...
Загрузка файла photo.jpg с диска...
Отображаю photo.jpg
Отображаю photo.jpg ← загрузки нет, переиспользуем
Protection Proxy — проверка прав
interface UserServiceInterface
{
public function getUsers(): array;
public function deleteUser(int $id): void;
}
class UserService implements UserServiceInterface
{
public function getUsers(): array
{
echo "Получаю список пользователей" . PHP_EOL;
return [['id' => 1, 'name' => 'John']];
}
public function deleteUser(int $id): void
{
echo "Удаляю пользователя {$id}" . PHP_EOL;
}
}
// Прокси проверяет права перед каждым вызовом
class UserServiceProxy implements UserServiceInterface
{
public function __construct(
private UserService $service,
private string $role,
) {}
public function getUsers(): array
{
// Просмотр доступен всем
return $this->service->getUsers();
}
public function deleteUser(int $id): void
{
// Удаление только для админа
if ($this->role !== 'admin') {
throw new \RuntimeException('Доступ запрещён — нужна роль admin');
}
$this->service->deleteUser($id);
}
}
// Обычный пользователь
$proxy = new UserServiceProxy(new UserService(), role: 'user');
$proxy->getUsers(); // OK
$proxy->deleteUser(1); // RuntimeException: Доступ запрещён
// Админ
$proxy = new UserServiceProxy(new UserService(), role: 'admin');
$proxy->deleteUser(1); // OK — Удаляю пользователя 1
Caching Proxy — кэширование
interface ProductServiceInterface
{
public function getProduct(int $id): array;
}
class ProductService implements ProductServiceInterface
{
public function getProduct(int $id): array
{
echo "Запрос в БД для product {$id}" . PHP_EOL;
return ['id' => $id, 'name' => 'iPhone 15', 'price' => 999];
}
}
class CachingProductProxy implements ProductServiceInterface
{
private array $cache = [];
public function __construct(
private ProductService $service
) {}
public function getProduct(int $id): array
{
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->service->getProduct($id);
} else {
echo "Из кэша для product {$id}" . PHP_EOL;
}
return $this->cache[$id];
}
}
$proxy = new CachingProductProxy(new ProductService());
$proxy->getProduct(1); // Запрос в БД
$proxy->getProduct(1); // Из кэша
$proxy->getProduct(1); // Из кэша
$proxy->getProduct(2); // Запрос в БД
```
Вывод:
```
Запрос в БД для product 1
Из кэша для product 1
Из кэша для product 1
Запрос в БД для product 2
Отличие от Decorator
Внешне очень похожи — оба оборачивают объект. Но цель разная:
- Proxy — контролирует доступ к объекту. Часто сам управляет жизненным циклом реального объекта (создаёт его, кэширует).
- Decorator — добавляет новое поведение. Реальный объект всегда существует и передаётся снаружи.
Поведенческие паттерны
Если структурные паттерны отвечают на вопрос “как объекты связать”, то поведенческие отвечают на вопрос “как объекты взаимодействуют и распределяют ответственность между собой”.
Паттерн State (Состояние)
State — объект меняет своё поведение в зависимости от внутреннего состояния. Со стороны выглядит как будто объект меняет свой класс.
Простой пример
interface State
{
public function toNext(Task $task);
public function getStatus();
}
class Task
{
private State $state;
public function getState(): State
{
return $this->state;
}
public function setState(State $state): void
{
$this->state = $state;
}
// Static Factory — создаёт таску с начальным состоянием
public static function make(): Task
{
$self = new self();
$self->setState(new Created());
return $self;
}
public function proceedToNext(): void
{
$this->state->toNext($this);
}
}
class Created implements State
{
public function toNext(Task $task): void
{
$task->setState(new Process());
}
public function getStatus(): string
{
return 'Created';
}
}
class Process implements State
{
public function toNext(Task $task): void
{
$task->setState(new Done());
}
public function getStatus(): string
{
return 'Test'; // на скрине у тебя Test, видимо стадия тестирования
}
}
class Done implements State
{
public function toNext(Task $task): void
{
// конечное состояние — дальше некуда
}
public function getStatus(): string
{
return 'Done';
}
}
// Использование
$task = Task::make();
var_dump($task->getState()->getStatus()); // Created
$task->proceedToNext();
var_dump($task->getState()->getStatus()); // Test
$task->proceedToNext();
var_dump($task->getState()->getStatus()); // Done
$task->proceedToNext(); // ничего не происходит — Done конечное состояние
var_dump($task->getState()->getStatus()); // Done
```
Вывод:
```
string(7) "Created"
string(4) "Test"
string(4) "Done"
string(4) "Done"
Цепочка переходов: Created → Process(Test) → Done. На стадии Done метод toNext() пустой — дальше идти некуда.
Проблема без State
class Order
{
private string $status = 'new';
public function pay(): void
{
if ($this->status === 'new') {
echo "Оплачиваем заказ" . PHP_EOL;
$this->status = 'paid';
} elseif ($this->status === 'paid') {
echo "Заказ уже оплачен" . PHP_EOL;
} elseif ($this->status === 'cancelled') {
echo "Нельзя оплатить отменённый заказ" . PHP_EOL;
}
}
public function ship(): void
{
if ($this->status === 'new') {
echo "Нельзя отправить неоплаченный заказ" . PHP_EOL;
} elseif ($this->status === 'paid') {
echo "Отправляем заказ" . PHP_EOL;
$this->status = 'shipped';
} elseif ($this->status === 'cancelled') {
echo "Нельзя отправить отменённый заказ" . PHP_EOL;
}
}
public function cancel(): void
{
if ($this->status === 'new') {
echo "Отменяем заказ" . PHP_EOL;
$this->status = 'cancelled';
} elseif ($this->status === 'paid') {
echo "Отменяем и возвращаем деньги" . PHP_EOL;
$this->status = 'cancelled';
} elseif ($this->status === 'shipped') {
echo "Нельзя отменить отправленный заказ" . PHP_EOL;
}
}
}
Куча if/else в каждом методе. Добавляешь новый статус — лезешь во все методы и добавляешь ещё один elseif. Это быстро превращается в кашу.
Решение с State
// Интерфейс состояния
interface OrderState
{
public function pay(Order $order): void;
public function ship(Order $order): void;
public function cancel(Order $order): void;
}
// Состояние: новый заказ
class NewOrderState implements OrderState
{
public function pay(Order $order): void
{
echo "Оплачиваем заказ" . PHP_EOL;
$order->setState(new PaidOrderState());
}
public function ship(Order $order): void
{
echo "Нельзя отправить неоплаченный заказ" . PHP_EOL;
}
public function cancel(Order $order): void
{
echo "Отменяем заказ" . PHP_EOL;
$order->setState(new CancelledOrderState());
}
}
// Состояние: оплачен
class PaidOrderState implements OrderState
{
public function pay(Order $order): void
{
echo "Заказ уже оплачен" . PHP_EOL;
}
public function ship(Order $order): void
{
echo "Отправляем заказ" . PHP_EOL;
$order->setState(new ShippedOrderState());
}
public function cancel(Order $order): void
{
echo "Отменяем и возвращаем деньги" . PHP_EOL;
$order->setState(new CancelledOrderState());
}
}
// Состояние: отправлен
class ShippedOrderState implements OrderState
{
public function pay(Order $order): void
{
echo "Заказ уже оплачен и отправлен" . PHP_EOL;
}
public function ship(Order $order): void
{
echo "Заказ уже отправлен" . PHP_EOL;
}
public function cancel(Order $order): void
{
echo "Нельзя отменить отправленный заказ" . PHP_EOL;
}
}
// Состояние: отменён
class CancelledOrderState implements OrderState
{
public function pay(Order $order): void
{
echo "Нельзя оплатить отменённый заказ" . PHP_EOL;
}
public function ship(Order $order): void
{
echo "Нельзя отправить отменённый заказ" . PHP_EOL;
}
public function cancel(Order $order): void
{
echo "Заказ уже отменён" . PHP_EOL;
}
}
// Контекст — сам заказ
class Order
{
private OrderState $state;
public function __construct()
{
$this->state = new NewOrderState(); // начальное состояние
}
public function setState(OrderState $state): void
{
$this->state = $state;
}
// Делегирует вызовы текущему состоянию
public function pay(): void { $this->state->pay($this); }
public function ship(): void { $this->state->ship($this); }
public function cancel(): void { $this->state->cancel($this); }
}
// Использование
$order = new Order();
$order->ship(); // Нельзя отправить неоплаченный заказ
$order->pay(); // Оплачиваем заказ
$order->pay(); // Заказ уже оплачен
$order->ship(); // Отправляем заказ
$order->cancel(); // Нельзя отменить отправленный заказ
```
---
### Вывод
```
Нельзя отправить неоплаченный заказ
Оплачиваем заказ
Заказ уже оплачен
Отправляем заказ
Нельзя отменить отправленный заказ
Добавляем новый статус — не трогаем старый код
// Просто создаём новый класс
class RefundedOrderState implements OrderState
{
public function pay(Order $order): void
{
echo "Нельзя оплатить возвращённый заказ" . PHP_EOL;
}
public function ship(Order $order): void
{
echo "Нельзя отправить возвращённый заказ" . PHP_EOL;
}
public function cancel(Order $order): void
{
echo "Заказ уже возвращён" . PHP_EOL;
}
}
// И используем в нужном состоянии
class PaidOrderState implements OrderState
{
public function cancel(Order $order): void
{
echo "Возвращаем деньги" . PHP_EOL;
$order->setState(new RefundedOrderState()); // вместо CancelledOrderState
}
}
есть интересные отличия между 2-мя примерами:
1. Static Factory внутри контекста — у тебя Task::make() создаёт таску сразу с начальным состоянием. В моём примере это делалось в конструкторе. Твой вариант чище и читабельнее.
2. Один метод toNext() вместо нескольких — у тебя состояние умеет только одно — перейти к следующему. В моём примере у каждого состояния было три метода pay(), ship(), cancel(). Твой вариант проще потому что таска идёт линейно:
Created → Process → Done
В моём заказ мог ветвиться:
New → Paid → Shipped
↓
Cancelled
3. getStatus() — удобный метод чтобы снаружи узнать текущий статус не зная какой именно класс состояния сейчас активен.
По сути пример с Task — это упрощённый State для линейного жизненного цикла, Order — для разветвлённого. Оба правильные, просто под разные задачи.
Паттерн Strategy (Стратегия)
Strategy — определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Выбираешь нужный алгоритм в рантайме не меняя клиентский код.
Проблема без Strategy
class OrderService
{
public function pay(int $amount, string $method): void
{
if ($method === 'stripe') {
echo "Оплата через Stripe: {$amount}$" . PHP_EOL;
} elseif ($method === 'paypal') {
echo "Оплата через PayPal: {$amount}$" . PHP_EOL;
} elseif ($method === 'crypto') {
echo "Оплата через Crypto: {$amount}$" . PHP_EOL;
}
// добавляешь новый метод — лезешь сюда и добавляешь elseif
}
}
Решение с Strategy
// Интерфейс стратегии
interface PaymentStrategy
{
public function pay(int $amount): void;
}
// Конкретные стратегии
class StripeStrategy implements PaymentStrategy
{
public function pay(int $amount): void
{
echo "Оплата через Stripe: {$amount}$" . PHP_EOL;
}
}
class PayPalStrategy implements PaymentStrategy
{
public function pay(int $amount): void
{
echo "Оплата через PayPal: {$amount}$" . PHP_EOL;
}
}
class CryptoStrategy implements PaymentStrategy
{
public function pay(int $amount): void
{
echo "Оплата через Crypto: {$amount}$" . PHP_EOL;
}
}
// Контекст — использует стратегию
class OrderService
{
private PaymentStrategy $strategy;
public function setStrategy(PaymentStrategy $strategy): void
{
$this->strategy = $strategy;
}
public function pay(int $amount): void
{
$this->strategy->pay($amount);
}
}
// Использование
$order = new OrderService();
$order->setStrategy(new StripeStrategy());
$order->pay(100); // Оплата через Stripe: 100$
$order->setStrategy(new PayPalStrategy());
$order->pay(200); // Оплата через PayPal: 200$
$order->setStrategy(new CryptoStrategy());
$order->pay(300); // Оплата через Crypto: 300$
Стратегию можно передавать через конструктор
class OrderService
{
public function __construct(
private PaymentStrategy $strategy
) {}
public function pay(int $amount): void
{
$this->strategy->pay($amount);
}
}
// Выбираем стратегию при создании
$order = new OrderService(new StripeStrategy());
$order->pay(100);
Пример с видео
interface Definer
{
public function define($arg): string;
}
class IntDefiner implements Definer
{
public function define($arg): string
{
return $arg . ' from int strategy';
}
}
class BoolDefiner implements Definer
{
public function define($arg): string
{
return $arg . ' from bool strategy';
}
}
// Контекст
class Data
{
private $arg;
private Definer $definer;
public function __construct(Definer $definer)
{
$this->definer = $definer;
}
public function setArg($arg): void
{
$this->arg = $arg;
}
public function executeStrategy(): string
{
return $this->definer->define($this->arg);
}
}
// Использование
$data = new Data(new IntDefiner());
$data->setArg('some arg for first');
var_dump($data->executeStrategy());
// string "some arg for first from int strategy"
$data = new Data(new BoolDefiner());
$data->setArg('some arg for first');
var_dump($data->executeStrategy());
// string "some arg for first from bool strategy"
Паттерн Null Object
Null Object — вместо того чтобы возвращать null и везде проверять if ($object !== null), возвращаешь специальный объект который ничего не делает но реализует тот же интерфейс.
То есть благодаря нему можем вызывать объект не боясь что у него ничего нет.
Проблема без Null Object
class UserRepository
{
public function find(int $id): ?User
{
// пользователь не найден
return null;
}
}
// Везде в коде нужно проверять на null
$user = $repository->find(1);
if ($user !== null) {
echo $user->getName();
}
if ($user !== null) {
echo $user->getEmail();
}
if ($user !== null) {
$user->sendNotification('Привет');
}
Чем больше мест где используется $user — тем больше проверок на null. Забыл проверить — получаешь Call to a member function getName() on null.
Решение с Null Object
interface UserInterface
{
public function getName(): string;
public function getEmail(): string;
public function sendNotification(string $message): void;
public function isNull(): bool;
}
// Реальный пользователь
class User implements UserInterface
{
public function __construct(
private string $name,
private string $email,
) {}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function sendNotification(string $message): void
{
echo "Отправляю '{$message}' на {$this->email}" . PHP_EOL;
}
public function isNull(): bool
{
return false;
}
}
// Null объект — ничего не делает, но не падает
class NullUser implements UserInterface
{
public function getName(): string
{
return 'Гость';
}
public function getEmail(): string
{
return '';
}
public function sendNotification(string $message): void
{
// ничего не делаем
}
public function isNull(): bool
{
return true;
}
}
class UserRepository
{
private array $users = [
1 => ['name' => 'John', 'email' => '[email protected]'],
];
public function find(int $id): UserInterface
{
if (isset($this->users[$id])) {
return new User($this->users[$id]['name'], $this->users[$id]['email']);
}
return new NullUser(); // вместо null
}
}
// Использование — никаких проверок на null
$repository = new UserRepository();
$user = $repository->find(1); // реальный пользователь
echo $user->getName() . PHP_EOL; // John
$user->sendNotification('Привет'); // Отправляю 'Привет' на [email protected]
$user = $repository->find(999); // пользователь не найден
echo $user->getName() . PHP_EOL; // Гость
$user->sendNotification('Привет'); // тишина — ничего не происходит
// Если нужно проверить — isNull() вместо !== null
if ($user->isNull()) {
echo "Пользователь не найден" . PHP_EOL;
}
```
---
### Вывод
```
John
Отправляю 'Привет' на [email protected]
Гость
Пользователь не найден
Где встречается в реальности
В Laravel optional() хелпер — это по сути Null Object:
// Без optional — упадёт если $user null
$name = $user->profile->name;
// С optional — вернёт null но не упадёт
$name = optional($user->profile)->name;
Также в Laravel если связь не найдена через ->withDefault() — возвращается пустая модель вместо null, что тоже является Null Object.
Паттерн Command (Команда)
Command — оборачивает запрос в объект. Это позволяет откладывать выполнение, ставить в очередь, логировать и отменять операции.
Проблема без Command
class OrderService
{
public function createOrder(): void
{
// логика создания
}
public function cancelOrder(): void
{
// логика отмены
}
}
// Нет истории, нет отмены, нет очереди
$service = new OrderService();
$service->createOrder();
Решение с Command
// Интерфейс команды
interface Command
{
public function execute(): void;
public function undo(): void;
}
// Получатель — тот кто реально выполняет работу
class OrderService
{
public function create(int $orderId): void
{
echo "Заказ #{$orderId} создан" . PHP_EOL;
}
public function cancel(int $orderId): void
{
echo "Заказ #{$orderId} отменён" . PHP_EOL;
}
public function sendEmail(int $orderId): void
{
echo "Email для заказа #{$orderId} отправлен" . PHP_EOL;
}
}
// Конкретные команды
class CreateOrderCommand implements Command
{
public function __construct(
private OrderService $service,
private int $orderId,
) {}
public function execute(): void
{
$this->service->create($this->orderId);
}
public function undo(): void
{
$this->service->cancel($this->orderId);
}
}
class SendEmailCommand implements Command
{
public function __construct(
private OrderService $service,
private int $orderId,
) {}
public function execute(): void
{
$this->service->sendEmail($this->orderId);
}
public function undo(): void
{
echo "Email для заказа #{$this->orderId} отозвать нельзя" . PHP_EOL;
}
}
// Invoker — запускает команды и хранит историю
class CommandInvoker
{
private array $history = [];
public function execute(Command $command): void
{
$command->execute();
$this->history[] = $command;
}
// Отменить последнюю команду
public function undo(): void
{
if (empty($this->history)) {
echo "Нечего отменять" . PHP_EOL;
return;
}
$command = array_pop($this->history);
$command->undo();
}
// Отменить все команды в обратном порядке
public function undoAll(): void
{
while (!empty($this->history)) {
$this->undo();
}
}
}
// Использование
$service = new OrderService();
$invoker = new CommandInvoker();
$invoker->execute(new CreateOrderCommand($service, 42));
$invoker->execute(new SendEmailCommand($service, 42));
echo PHP_EOL . "--- Отменяем ---" . PHP_EOL;
$invoker->undo(); // отменяем email
$invoker->undo(); // отменяем создание заказа
$invoker->undo(); // нечего отменять
```
---
### Вывод
```
Заказ #42 создан
Email для заказа #42 отправлен
--- Отменяем ---
Email для заказа #42 отозвать нельзя
Заказ #42 отменён
Нечего отменять
Очередь команд
class CommandQueue
{
private array $queue = [];
public function add(Command $command): void
{
$this->queue[] = $command;
}
public function run(): void
{
foreach ($this->queue as $command) {
$command->execute();
}
$this->queue = [];
}
}
// Набираем команды и запускаем все разом
$queue = new CommandQueue();
$queue->add(new CreateOrderCommand($service, 1));
$queue->add(new CreateOrderCommand($service, 2));
$queue->add(new SendEmailCommand($service, 1));
$queue->add(new SendEmailCommand($service, 2));
$queue->run();
Где встречается в реальности
В Laravel Jobs — это классический Command. Каждый Job оборачивает задачу в объект, ставится в очередь и выполняется позже. php artisan команды тоже построены на этом паттерне — каждая artisan команда это отдельный класс с методом handle().
Паттерн Interpreter (Интерпретатор)
Interpreter — определяет грамматику языка и интерпретирует его выражения. Используется когда нужно обрабатывать простые языки или выражения — математические формулы, SQL запросы, поисковые фильтры.
Проблема без Interpreter
// Хотим вычислять выражения вида "2 + 3 - 1"
// Без паттерна — одна большая функция с кучей логики
function calculate(string $expression): int
{
// парсим, считаем — всё в одном месте, сложно расширять
}
Решение с Interpreter
// Интерфейс выражения
interface Expression
{
public function interpret(): int;
}
// Терминальное выражение — число (дальше не делится)
class NumberExpression implements Expression
{
public function __construct(
private int $number
) {}
public function interpret(): int
{
return $this->number;
}
}
// Нетерминальные выражения — операции над другими выражениями
class AddExpression implements Expression
{
public function __construct(
private Expression $left,
private Expression $right,
) {}
public function interpret(): int
{
return $this->left->interpret() + $this->right->interpret();
}
}
class SubtractExpression implements Expression
{
public function __construct(
private Expression $left,
private Expression $right,
) {}
public function interpret(): int
{
return $this->left->interpret() - $this->right->interpret();
}
}
class MultiplyExpression implements Expression
{
public function __construct(
private Expression $left,
private Expression $right,
) {}
public function interpret(): int
{
return $this->left->interpret() * $this->right->interpret();
}
}
// Строим дерево выражений вручную
// (2 + 3) * (10 - 4)
$expression = new MultiplyExpression(
new AddExpression(
new NumberExpression(2),
new NumberExpression(3),
),
new SubtractExpression(
new NumberExpression(10),
new NumberExpression(4),
),
);
echo $expression->interpret() . PHP_EOL; // 30
Более реальный пример — фильтры товаров
interface Filter
{
public function apply(array $products): array;
}
// Фильтр по цене
class PriceFilter implements Filter
{
public function __construct(
private int $min,
private int $max,
) {}
public function apply(array $products): array
{
return array_filter($products, function ($product) {
return $product['price'] >= $this->min
&& $product['price'] <= $this->max;
});
}
}
// Фильтр по категории
class CategoryFilter implements Filter
{
public function __construct(
private string $category
) {}
public function apply(array $products): array
{
return array_filter($products, function ($product) {
return $product['category'] === $this->category;
});
}
}
// Фильтр по наличию
class InStockFilter implements Filter
{
public function apply(array $products): array
{
return array_filter($products, fn($p) => $p['in_stock'] === true);
}
}
// Составные фильтры — AND и OR
class AndFilter implements Filter
{
public function __construct(
private Filter $left,
private Filter $right,
) {}
public function apply(array $products): array
{
return $this->right->apply(
$this->left->apply($products)
);
}
}
class OrFilter implements Filter
{
public function __construct(
private Filter $left,
private Filter $right,
) {}
public function apply(array $products): array
{
$left = $this->left->apply($products);
$right = $this->right->apply($products);
return array_unique(array_merge($left, $right), SORT_REGULAR);
}
}
// Данные
$products = [
['name' => 'iPhone 15', 'price' => 999, 'category' => 'phones', 'in_stock' => true],
['name' => 'Samsung S24','price' => 799, 'category' => 'phones', 'in_stock' => false],
['name' => 'MacBook', 'price' => 1299, 'category' => 'laptops', 'in_stock' => true],
['name' => 'iPad', 'price' => 599, 'category' => 'tablets', 'in_stock' => true],
['name' => 'AirPods', 'price' => 199, 'category' => 'audio', 'in_stock' => true],
];
// Телефоны в наличии ценой до 900$
$filter = new AndFilter(
new AndFilter(
new CategoryFilter('phones'),
new InStockFilter(),
),
new PriceFilter(0, 900),
);
$result = $filter->apply($products);
foreach ($result as $product) {
echo "{$product['name']} — {$product['price']}$" . PHP_EOL;
}
// Samsung S24 не попадает — нет в наличии
// iPhone 15 не попадает — дороже 900$
Где встречается в реальности
Laravel Query Builder — это Interpreter. Каждый ->where(), ->orderBy(), ->limit() строит дерево выражений которое в конце интерпретируется в SQL. Symfony Expression Language — готовая реализация паттерна для вычисления выражений вида user.age > 18 and user.active == true.
Когда использовать
Interpreter нужен когда есть повторяющиеся задачи которые можно описать простым языком — фильтры, правила валидации, условия доступа, математические выражения. Для сложных языков лучше использовать готовые парсеры — писать полноценный интерпретатор вручную сложно и долго.
Паттерн Specification (Спецификация)
Specification — позволяет описывать бизнес-правила в отдельных классах и комбинировать их через AND, OR, NOT. По сути это улучшенная версия Interpreter специально для бизнес-правил и фильтрации.
Specification — проверяет соответствие объекта бизнес-правилам. Всегда возвращает true или false. Про фильтрацию и валидацию.
Проблема без Specification
class UserService
{
public function getPremiumAdultActiveUsers(array $users): array
{
return array_filter($users, function ($user) {
// бизнес-правила размазаны прямо в коде
return $user->age >= 18
&& $user->isPremium === true
&& $user->isActive === true;
});
}
public function getAdultUsers(array $users): array
{
return array_filter($users, function ($user) {
return $user->age >= 18; // дублирование правила
});
}
}
Правила дублируются, их сложно переиспользовать и тестировать.
Решение
// Базовый интерфейс спецификации
interface Specification
{
public function isSatisfiedBy(mixed $candidate): bool;
public function and(Specification $other): Specification
{
return new AndSpecification($this, $other);
}
public function or(Specification $other): Specification
{
return new OrSpecification($this, $other);
}
public function not(): Specification
{
return new NotSpecification($this);
}
}
// Составные спецификации — AND, OR, NOT
class AndSpecification implements Specification
{
public function __construct(
private Specification $left,
private Specification $right,
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->left->isSatisfiedBy($candidate)
&& $this->right->isSatisfiedBy($candidate);
}
}
class OrSpecification implements Specification
{
public function __construct(
private Specification $left,
private Specification $right,
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->left->isSatisfiedBy($candidate)
|| $this->right->isSatisfiedBy($candidate);
}
}
class NotSpecification implements Specification
{
public function __construct(
private Specification $spec
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
return !$this->spec->isSatisfiedBy($candidate);
}
}
// Модель пользователя
class User
{
public function __construct(
public string $name,
public int $age,
public bool $isPremium,
public bool $isActive,
public string $country,
) {}
}
// Конкретные спецификации — каждое правило в отдельном классе
class IsAdultSpecification implements Specification
{
public function isSatisfiedBy(mixed $user): bool
{
return $user->age >= 18;
}
}
class IsPremiumSpecification implements Specification
{
public function isSatisfiedBy(mixed $user): bool
{
return $user->isPremium === true;
}
}
class IsActiveSpecification implements Specification
{
public function isSatisfiedBy(mixed $user): bool
{
return $user->isActive === true;
}
}
class IsFromCountrySpecification implements Specification
{
public function __construct(
private string $country
) {}
public function isSatisfiedBy(mixed $user): bool
{
return $user->country === $this->country;
}
}
// Фильтрация через спецификацию
class UserCollection
{
public function __construct(
private array $users
) {}
public function filter(Specification $spec): array
{
return array_values(array_filter(
$this->users,
fn($user) => $spec->isSatisfiedBy($user)
));
}
}
// Данные
$users = new UserCollection([
new User('John', 25, true, true, 'Ukraine'),
new User('Jane', 17, true, true, 'Ukraine'),
new User('Bob', 30, false, true, 'Poland'),
new User('Alice', 22, true, false, 'Ukraine'),
new User('Mike', 35, true, true, 'Ukraine'),
]);
$isAdult = new IsAdultSpecification();
$isPremium = new IsPremiumSpecification();
$isActive = new IsActiveSpecification();
$isUkraine = new IsFromCountrySpecification('Ukraine');
// Комбинируем правила
$premiumActiveAdults = $isAdult->and($isPremium)->and($isActive);
$result = $users->filter($premiumActiveAdults);
foreach ($result as $user) {
echo "{$user->name} — {$user->age} лет" . PHP_EOL;
}
// John — 25 лет
// Mike — 35 лет
Комбинируем по-разному — правила не меняем
// Взрослые из Украины
$result = $users->filter(
$isAdult->and($isUkraine)
);
// Премиум ИЛИ из Украины
$result = $users->filter(
$isPremium->or($isUkraine)
);
// НЕ премиум
$result = $users->filter(
$isPremium->not()
);
// Активные взрослые из Украины которые НЕ премиум
$result = $users->filter(
$isAdult->and($isActive)->and($isUkraine)->and($isPremium->not())
);
Паттерн Chain of Responsibility (Цепочка обязанностей)
Chain of Responsibility — передаёт запрос по цепочке обработчиков. Каждый обработчик решает — обработать запрос или передать дальше по цепочке. Chain of Responsibility — цепочка прерывается когда обработчик решает не передавать дальше.
Проблема без Chain
class OrderValidator
{
public function validate(array $order): bool
{
// всё в одном месте — сложно расширять
if (empty($order['email'])) {
echo "Email обязателен" . PHP_EOL;
return false;
}
if ($order['amount'] <= 0) {
echo "Сумма должна быть больше нуля" . PHP_EOL;
return false;
}
if ($order['amount'] > 10000) {
echo "Сумма превышает лимит" . PHP_EOL;
return false;
}
return true;
}
}
Добавляешь новую проверку — лезешь в этот класс. Порядок проверок жёстко зашит.
Решение с Chain
// Базовый обработчик
abstract class OrderHandler
{
private ?OrderHandler $next = null;
// Устанавливаем следующий обработчик и возвращаем его
// чтобы можно было строить цепочку
public function setNext(OrderHandler $handler): OrderHandler
{
$this->next = $handler;
return $handler;
}
// Передать дальше по цепочке
protected function passToNext(array $order): bool
{
if ($this->next !== null) {
return $this->next->handle($order);
}
// Дошли до конца цепочки — всё ок
echo "Заказ прошёл все проверки" . PHP_EOL;
return true;
}
abstract public function handle(array $order): bool;
}
// Конкретные обработчики
class EmailHandler extends OrderHandler
{
public function handle(array $order): bool
{
if (empty($order['email'])) {
echo "Email обязателен" . PHP_EOL;
return false;
}
echo "Email OK" . PHP_EOL;
return $this->passToNext($order);
}
}
class AmountHandler extends OrderHandler
{
public function handle(array $order): bool
{
if ($order['amount'] <= 0) {
echo "Сумма должна быть больше нуля" . PHP_EOL;
return false;
}
echo "Amount OK" . PHP_EOL;
return $this->passToNext($order);
}
}
class LimitHandler extends OrderHandler
{
public function handle(array $order): bool
{
if ($order['amount'] > 10000) {
echo "Сумма превышает лимит" . PHP_EOL;
return false;
}
echo "Limit OK" . PHP_EOL;
return $this->passToNext($order);
}
}
class StockHandler extends OrderHandler
{
public function handle(array $order): bool
{
if (empty($order['product_id'])) {
echo "Товар не указан" . PHP_EOL;
return false;
}
echo "Stock OK" . PHP_EOL;
return $this->passToNext($order);
}
}
// Строим цепочку
$email = new EmailHandler();
$amount = new AmountHandler();
$limit = new LimitHandler();
$stock = new StockHandler();
$email->setNext($amount)
->setNext($limit)
->setNext($stock);
// Запускаем с начала цепочки
echo "--- Валидный заказ ---" . PHP_EOL;
$email->handle([
'email' => '[email protected]',
'amount' => 500,
'product_id' => 42,
]);
echo PHP_EOL . "--- Нет email ---" . PHP_EOL;
$email->handle([
'email' => '',
'amount' => 500,
'product_id' => 42,
]);
echo PHP_EOL . "--- Превышен лимит ---" . PHP_EOL;
$email->handle([
'email' => '[email protected]',
'amount' => 99999,
'product_id' => 42,
]);
```
---
### Вывод
```
--- Валидный заказ ---
Email OK
Amount OK
Limit OK
Stock OK
Заказ прошёл все проверки
--- Нет email ---
Email обязателен
--- Превышен лимит ---
Email OK
Amount OK
Сумма превышает лимит
Меняем порядок — просто перестраиваем цепочку
// Сначала проверяем лимит потом email
$limit->setNext($email)
->setNext($amount)
->setNext($stock);
$limit->handle($order);
Пример с видео
interface TaskInterface
{
public function getArray(): array;
}
class DevTask implements TaskInterface
{
public function getArray(): array
{
return ['type' => 'dev', 'level' => 'junior'];
}
}
// Базовый Handler
abstract class Handler
{
private ?Handler $successor;
public function __construct(?Handler $successor)
{
$this->successor = $successor;
}
// final — нельзя переопределить в наследниках
final public function handle(TaskInterface $task): ?array
{
$proceed = $this->processing($task);
// Передаём дальше только если текущий вернул null
if ($proceed === null && $this->successor) {
$proceed = $this->successor->handle($task);
}
return $proceed;
}
// Каждый наследник реализует только это
abstract public function processing(TaskInterface $task): ?array;
}
// Обработчики
class Junior extends Handler
{
public function processing(TaskInterface $task): ?array
{
echo "Junior обрабатывает задачу" . PHP_EOL;
return ['handler' => 'Junior', 'result' => 'done'];
// если вернуть null — передаст дальше к Middle
}
}
class Middle extends Handler
{
public function processing(TaskInterface $task): ?array
{
echo "Middle обрабатывает задачу" . PHP_EOL;
return ['handler' => 'Middle', 'result' => 'done'];
}
}
class Senior extends Handler
{
public function processing(TaskInterface $task): ?array
{
echo "Senior обрабатывает задачу" . PHP_EOL;
return ['handler' => 'Senior', 'result' => 'done'];
}
}
class Jun extends Handler
{
public function processing(TaskInterface $task): ?array
{
return null; // не может обработать — передаёт дальше
}
}
// Строим цепочку через конструктор
$senior = new Senior(successor: null); // последний в цепочке
$mid = new Middle($senior); // если не справится — идёт к Senior
$jun = new Jun($mid); // если не справится — идёт к Middle
var_dump($jun->handle(new DevTask()));
Главное отличие от первого примера
В первом цепочка строилась через setNext():
$email->setNext($amount)->setNext($limit);
Здесь цепочка строится через конструктор:
$senior = new Senior(null);
$mid = new Middle($senior);
$jun = new Jun($mid);
И логика передачи разная:
В первом — передаёт дальше всегда если сам обработал:
return $this->passToNext($order); // всегда идём дальше
Здесь — передаёт дальше только если processing() вернул null:
if ($proceed === null && $this->successor) {
$proceed = $this->successor->handle($task);
}
Это элегантнее — обработчик просто возвращает null если не может справиться, и цепочка сама решает идти дальше. Не нужно явно вызывать passToNext().
Iterator
Iterator — это паттерн который даёт способ последовательно обходить элементы коллекции, не зная как она устроена внутри.
Главная идея
Коллекция не должна знать как её обходят. Обход — это отдельная ответственность.
interface WorkerInterface
{
public function getName(): string;
}
class Worker implements WorkerInterface
{
public function __construct(
private string $name
) {}
public function getName(): string
{
return $this->name;
}
}
class WorkerList
{
private array $list = [];
private int $index = 0;
public function setList(array $workers): void
{
$this->list = $workers;
$this->index = 0;
}
public function next(): void
{
$this->index++;
}
public function prev(): void
{
$this->index--;
}
public function rewind(): void
{
$this->index = 0;
}
public function getByIndex(): WorkerInterface
{
if (!isset($this->list[$this->index])) {
throw new \OutOfBoundsException("Индекс {$this->index} не существует");
}
return $this->list[$this->index];
}
}
// Использование
$worker = new Worker(name: 'Boris');
$worker2 = new Worker(name: 'Bob');
$worker3 = new Worker(name: 'Kate');
$workerList = new WorkerList();
$workerList->setList([$worker, $worker2, $worker3]);
// index = 0 → Boris
var_dump($workerList->getByIndex()->getName()); // string(5) "Boris"
$workerList->next(); // index = 1
var_dump($workerList->getByIndex()->getName()); // string(3) "Bob"
$workerList->next(); // index = 2
var_dump($workerList->getByIndex()->getName()); // string(4) "Kate"
$workerList->prev(); // index = 1
var_dump($workerList->getByIndex()->getName()); // string(3) "Bob"
$workerList->rewind(); // index = 0
var_dump($workerList->getByIndex()->getName()); // string(5) "Boris"
Три участника
- Коллекция (
WorkerList) — хранит элементы, не знает как их обходят. - Итератор — знает как обходить. В примере со скриншота итератор встроен прямо в коллекцию (
next(),prev(),getByIndex()). - Клиент — использует итератор не зная что внутри массив, база данных или дерево.
Зачем это нужно
Представь что завтра WorkerList перестанет хранить работников в массиве — начнёт загружать их из БД по одному. Клиентский код не изменится:
// Клиенту всё равно откуда данные
$workerList->next();
$workerList->getByIndex()->getName();
Поменялось только внутри WorkerList — снаружи всё то же самое. В этом и есть смысл паттерна — отделить способ обхода от самой коллекции.
Паттерн Mediator (Посредник)
Mediator — объект-посредник через которого общаются все остальные объекты. Убирает прямые связи между объектами — они не знают друг о друге, знают только о посреднике.
Проблема без Mediator
// Без посредника — каждый юзер напрямую знает о других
class User
{
public function __construct(public string $name) {}
// Юзер сам отправляет другому юзеру — прямая связь
public function sendTo(User $receiver, string $message): void
{
echo "{$this->name} → {$receiver->name}: {$message}" . PHP_EOL;
}
}
$john = new User('John');
$jane = new User('Jane');
$bob = new User('Bob');
$john->sendTo($jane, 'Привет'); // john знает о jane
$jane->sendTo($bob, 'Привет'); // jane знает о bob
$bob->sendTo($john, 'Привет'); // bob знает о john
Чем больше объектов — тем больше связей. 4 объекта = 12 связей. Изменяешь один — ломаешь другие.
Решение с Mediator
// Интерфейс посредника
interface ChatMediator
{
public function sendMessage(string $message, UserComponent $sender): void;
public function addUser(UserComponent $user): void;
}
// Конкретный посредник — чат
class ChatRoom implements ChatMediator
{
private array $users = [];
public function addUser(UserComponent $user): void
{
$this->users[] = $user;
}
public function sendMessage(string $message, UserComponent $sender): void
{
foreach ($this->users as $user) {
// Отправляем всем кроме отправителя
if ($user !== $sender) {
$user->receive($message, $sender->getName());
}
}
}
}
// Участник — знает только о посреднике
class UserComponent
{
public function __construct(
private string $name,
private ChatMediator $mediator,
) {}
public function getName(): string
{
return $this->name;
}
public function send(string $message): void
{
echo "{$this->name} отправляет: {$message}" . PHP_EOL;
$this->mediator->sendMessage($message, $this);
}
public function receive(string $message, string $from): void
{
echo "{$this->name} получает от {$from}: {$message}" . PHP_EOL;
}
}
// Использование
$chat = new ChatRoom();
$john = new UserComponent('John', $chat);
$jane = new UserComponent('Jane', $chat);
$bob = new UserComponent('Bob', $chat);
$chat->addUser($john);
$chat->addUser($jane);
$chat->addUser($bob);
$john->send('Всем привет!');
echo PHP_EOL;
$jane->send('Привет John!');
```
---
### Вывод
```
John отправляет: Всем привет!
Jane получает от John: Всем привет!
Bob получает от John: Всем привет!
Jane отправляет: Привет John!
John получает от Jane: Привет John!
Bob получает от Jane: Привет John!
Более реальный пример — форма с зависимыми полями
// Посредник формы
class FormMediator
{
private array $components = [];
public function register(string $name, FormComponent $component): void
{
$this->components[$name] = $component;
}
public function notify(string $event, FormComponent $sender): void
{
// Когда меняется страна — обновляем список городов
if ($event === 'country_changed') {
$this->components['city']->update($sender->getValue());
}
// Когда меняется тип доставки — показываем/скрываем адрес
if ($event === 'delivery_changed') {
$value = $sender->getValue();
if ($value === 'pickup') {
$this->components['address']->hide();
} else {
$this->components['address']->show();
}
}
}
}
class FormComponent
{
private mixed $value = null;
private bool $visible = true;
public function __construct(
private string $name,
private FormMediator $mediator,
) {}
public function setValue(mixed $value): void
{
$this->value = $value;
$this->mediator->notify("{$this->name}_changed", $this);
}
public function getValue(): mixed { return $this->value; }
public function hide(): void { $this->visible = false; echo "{$this->name} скрыт" . PHP_EOL; }
public function show(): void { $this->visible = true; echo "{$this->name} показан" . PHP_EOL; }
public function update(mixed $data): void
{
echo "{$this->name} обновлён данными: {$data}" . PHP_EOL;
}
}
// Использование
$mediator = new FormMediator();
$country = new FormComponent('country', $mediator);
$city = new FormComponent('city', $mediator);
$delivery = new FormComponent('delivery', $mediator);
$address = new FormComponent('address', $mediator);
$mediator->register('country', $country);
$mediator->register('city', $city);
$mediator->register('delivery', $delivery);
$mediator->register('address', $address);
$country->setValue('Ukraine'); // city обновлён данными: Ukraine
$delivery->setValue('pickup'); // address скрыт
$delivery->setValue('courier'); // address показан
Где встречается в реальности
Laravel Event система — это Mediator. event(new OrderCreated()) — посредник доставляет событие всем слушателям, отправитель не знает кто их слушает. В frontend фреймворках — Vuex, Redux — тоже Mediator, компоненты общаются через централизованное хранилище.
Паттерн Memento (Снимок)
Memento — сохраняет и восстанавливает предыдущее состояние объекта не нарушая инкапсуляцию. По сути это Ctrl+Z — история изменений.
Три участника
- Originator — объект чьё состояние сохраняем. Умеет создавать снимок и восстанавливаться из него.
- Memento — снимок состояния. Просто хранит данные.
- Caretaker — хранитель истории снимков. Не знает что внутри снимка — просто хранит и отдаёт.
Пример — текстовый редактор
// Memento — снимок состояния
class EditorMemento
{
public function __construct(
private string $content,
private int $cursorPosition,
) {}
public function getContent(): string { return $this->content; }
public function getCursorPosition(): int { return $this->cursorPosition; }
}
// Originator — редактор
class Editor
{
private string $content = '';
private int $cursorPosition = 0;
public function type(string $text): void
{
$this->content .= $text;
$this->cursorPosition = strlen($this->content);
echo "Текст: {$this->content}" . PHP_EOL;
}
public function delete(int $chars): void
{
$this->content = substr($this->content, 0, -$chars);
$this->cursorPosition = strlen($this->content);
echo "Текст: {$this->content}" . PHP_EOL;
}
// Создать снимок
public function save(): EditorMemento
{
return new EditorMemento($this->content, $this->cursorPosition);
}
// Восстановить из снимка
public function restore(EditorMemento $memento): void
{
$this->content = $memento->getContent();
$this->cursorPosition = $memento->getCursorPosition();
echo "Восстановлено: {$this->content}" . PHP_EOL;
}
public function getContent(): string { return $this->content; }
}
// Caretaker — хранит историю
class EditorHistory
{
private array $history = [];
public function push(EditorMemento $memento): void
{
$this->history[] = $memento;
}
public function pop(): ?EditorMemento
{
if (empty($this->history)) {
echo "История пуста" . PHP_EOL;
return null;
}
return array_pop($this->history);
}
}
// Использование
$editor = new Editor();
$history = new EditorHistory();
$history->push($editor->save()); // сохраняем пустое состояние
$editor->type('Привет ');
$history->push($editor->save()); // сохраняем
$editor->type('мир');
$history->push($editor->save()); // сохраняем
$editor->type('!!!');
echo PHP_EOL . "--- Ctrl+Z ---" . PHP_EOL;
$editor->restore($history->pop()); // Привет мир
$editor->restore($history->pop()); // Привет
$editor->restore($history->pop()); // (пусто)
$history->pop(); // История пуста
```
---
### Вывод
```
Текст: Привет
Текст: Привет мир
Текст: Привет мир!!!
--- Ctrl+Z ---
Восстановлено: Привет мир
Восстановлено: Привет
Восстановлено:
История пуста
Реальный пример — заказ с историей изменений
class OrderMemento
{
public function __construct(
private string $status,
private int $amount,
private string $comment,
) {}
public function getStatus(): string { return $this->status; }
public function getAmount(): int { return $this->amount; }
public function getComment(): string { return $this->comment; }
}
class Order
{
public function __construct(
private string $status = 'new',
private int $amount = 0,
private string $comment = '',
) {}
public function update(string $status, int $amount, string $comment): void
{
$this->status = $status;
$this->amount = $amount;
$this->comment = $comment;
}
public function save(): OrderMemento
{
return new OrderMemento($this->status, $this->amount, $this->comment);
}
public function restore(OrderMemento $memento): void
{
$this->status = $memento->getStatus();
$this->amount = $memento->getAmount();
$this->comment = $memento->getComment();
}
public function info(): void
{
echo "Статус: {$this->status} | Сумма: {$this->amount} | {$this->comment}" . PHP_EOL;
}
}
$order = new Order();
$history = new EditorHistory(); // переиспользуем Caretaker
$history->push($order->save());
$order->update('paid', 1000, 'Оплачен картой');
$order->info();
$history->push($order->save());
$order->update('shipped', 1000, 'Отправлен в Киев');
$order->info();
echo PHP_EOL . "--- Откатываем ---" . PHP_EOL;
$order->restore($history->pop());
$order->info(); // Оплачен картой
$order->restore($history->pop());
$order->info(); // Начальное состояние
Паттерн Observer (Наблюдатель)
Observer — объекты подписываются на события другого объекта и автоматически получают уведомления когда что-то меняется. Издатель не знает кто его слушает — просто рассылает событие всем подписчикам.
Три участника
- Publisher (издатель) — объект за которым наблюдают. Хранит список подписчиков и уведомляет их.
- Subscriber (подписчик) — объект который хочет знать о событиях издателя.
- Event — данные о событии которые передаются подписчикам.
Простой пример
// Интерфейс подписчика
interface Observer
{
public function update(string $event, mixed $data): void;
}
// Трейт издателя — можно добавить любому классу
trait Observable
{
private array $observers = [];
public function subscribe(string $event, Observer $observer): void
{
$this->observers[$event][] = $observer;
}
public function unsubscribe(string $event, Observer $observer): void
{
$this->observers[$event] = array_filter(
$this->observers[$event] ?? [],
fn($o) => $o !== $observer
);
}
public function notify(string $event, mixed $data = null): void
{
foreach ($this->observers[$event] ?? [] as $observer) {
$observer->update($event, $data);
}
}
}
// Издатель — заказ
class Order
{
use Observable;
private string $status = 'new';
public function __construct(
private int $id
) {}
public function pay(): void
{
$this->status = 'paid';
$this->notify('order.paid', ['id' => $this->id, 'status' => $this->status]);
}
public function ship(): void
{
$this->status = 'shipped';
$this->notify('order.shipped', ['id' => $this->id, 'status' => $this->status]);
}
public function cancel(): void
{
$this->status = 'cancelled';
$this->notify('order.cancelled', ['id' => $this->id, 'status' => $this->status]);
}
}
// Подписчики
class EmailNotificationObserver implements Observer
{
public function update(string $event, mixed $data): void
{
echo "Email: заказ #{$data['id']} — событие '{$event}'" . PHP_EOL;
}
}
class SmsNotificationObserver implements Observer
{
public function update(string $event, mixed $data): void
{
echo "SMS: заказ #{$data['id']} — событие '{$event}'" . PHP_EOL;
}
}
class LogObserver implements Observer
{
public function update(string $event, mixed $data): void
{
echo "LOG: [{$event}] заказ #{$data['id']} статус '{$data['status']}'" . PHP_EOL;
}
}
class InventoryObserver implements Observer
{
public function update(string $event, mixed $data): void
{
if ($event === 'order.paid') {
echo "Склад: резервируем товар для заказа #{$data['id']}" . PHP_EOL;
}
if ($event === 'order.cancelled') {
echo "Склад: возвращаем товар из заказа #{$data['id']}" . PHP_EOL;
}
}
}
// Использование
$order = new Order(42);
$email = new EmailNotificationObserver();
$sms = new SmsNotificationObserver();
$log = new LogObserver();
$inventory = new InventoryObserver();
// Подписываемся на конкретные события
$order->subscribe('order.paid', $email);
$order->subscribe('order.paid', $sms);
$order->subscribe('order.paid', $log);
$order->subscribe('order.paid', $inventory);
$order->subscribe('order.shipped', $email);
$order->subscribe('order.shipped', $log);
$order->subscribe('order.cancelled', $email);
$order->subscribe('order.cancelled', $inventory);
echo "--- Оплата ---" . PHP_EOL;
$order->pay();
echo PHP_EOL . "--- Отправка ---" . PHP_EOL;
$order->ship();
echo PHP_EOL . "--- Отписываем SMS ---" . PHP_EOL;
$order->unsubscribe('order.paid', $sms);
$order2 = new Order(43);
$order2->subscribe('order.paid', $email);
$order2->subscribe('order.cancelled', $inventory);
echo PHP_EOL . "--- Новый заказ отменён ---" . PHP_EOL;
$order2->cancel();
```
---
### Вывод
```
--- Оплата ---
Email: заказ #42 — событие 'order.paid'
SMS: заказ #42 — событие 'order.paid'
LOG: [order.paid] заказ #42 статус 'paid'
Склад: резервируем товар для заказа #42
--- Отправка ---
Email: заказ #42 — событие 'order.shipped'
LOG: [order.shipped] заказ #42 статус 'shipped'
--- Новый заказ отменён ---
Склад: возвращаем товар из заказа #43
Где встречается в реальности
В Laravel Events и Listeners — классический Observer. event(new OrderPaid($order)) рассылает событие всем слушателям. Eloquent Model Events — creating, updating, deleting — тоже Observer, модель уведомляет всех кто подписался на её события.
SplSubject и SplObserver
SplSubject и SplObserver — это встроенные в PHP интерфейсы для паттерна Observer. Не нужно писать свои интерфейсы — используешь готовые.
// PHP уже определяет эти интерфейсы:
interface SplSubject
{
public function attach(SplObserver $observer): void; // подписать
public function detach(SplObserver $observer): void; // отписать
public function notify(): void; // уведомить всех
}
interface SplObserver
{
public function update(SplSubject $subject): void;
}
Пример с Order
class Order implements SplSubject
{
private \SplObjectStorage $observers;
private string $status = 'new';
public function __construct(
private int $id
) {
$this->observers = new \SplObjectStorage();
}
public function attach(SplObserver $observer): void
{
$this->observers->attach($observer);
}
public function detach(SplObserver $observer): void
{
$this->observers->detach($observer);
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function pay(): void
{
$this->status = 'paid';
$this->notify();
}
public function getStatus(): string { return $this->status; }
public function getId(): int { return $this->id; }
}
// Подписчики
class EmailObserver implements SplObserver
{
public function update(SplSubject $subject): void
{
echo "Email: заказ #{$subject->getId()} — статус '{$subject->getStatus()}'" . PHP_EOL;
}
}
class LogObserver implements SplObserver
{
public function update(SplSubject $subject): void
{
echo "LOG: заказ #{$subject->getId()} — статус '{$subject->getStatus()}'" . PHP_EOL;
}
}
// Использование
$order = new Order(42);
$order->attach(new EmailObserver());
$order->attach(new LogObserver());
$order->pay();
// Email: заказ #42 — статус 'paid'
// LOG: заказ #42 — статус 'paid'
Отличие от моего предыдущего примера
В моём примере подписчик получал $event и $data — можно было подписаться на конкретное событие (order.paid, order.shipped).
В SplSubject — подписчик получает сам объект издателя и сам решает что с ним делать. Подписка на конкретные события не поддерживается из коробки.
// Мой вариант — подписка на конкретное событие
$order->subscribe('order.paid', $email);
// SplSubject — подписка на весь объект
$order->attach($email); // получит уведомление о ЛЮБОМ изменении
SplObjectStorage вместо массива — это специальная коллекция для объектов, автоматически исключает дубли и удобно удаляет через detach().
Паттерн Template Method (Шаблонный метод)
Template Method — определяет скелет алгоритма в базовом классе, оставляя детали подклассам. Структура алгоритма не меняется — меняются только отдельные шаги.
Проблема без Template Method
class CSVReportGenerator
{
public function generate(): void
{
// Получаем данные
$data = $this->getData();
// Форматируем
foreach ($data as $row) {
echo implode(',', $row) . PHP_EOL;
}
// Сохраняем
file_put_contents('report.csv', '...');
}
}
class HTMLReportGenerator
{
public function generate(): void
{
// Получаем данные — дублирование
$data = $this->getData();
// Форматируем по другому
echo "<table>";
foreach ($data as $row) {
echo "<tr><td>" . implode('</td><td>', $row) . "</td></tr>";
}
echo "</table>";
// Сохраняем — дублирование
file_put_contents('report.html', '...');
}
}
Структура одинаковая — получить данные, отформатировать, сохранить. Но дублируется в каждом классе.
Решение с Template Method
// Базовый класс — определяет скелет алгоритма
abstract class ReportGenerator
{
// Шаблонный метод — финальный, нельзя переопределить
final public function generate(): void
{
$data = $this->fetchData();
$formatted = $this->format($data);
$this->save($formatted);
$this->sendNotification(); // хук — необязательный шаг
}
// Обязательные шаги — подклассы должны реализовать
abstract protected function fetchData(): array;
abstract protected function format(array $data): string;
abstract protected function save(string $content): void;
// Хук — необязательный шаг с дефолтной реализацией
protected function sendNotification(): void
{
// По умолчанию ничего не делаем
}
}
// CSV отчёт
class CSVReport extends ReportGenerator
{
protected function fetchData(): array
{
echo "Получаем данные для CSV" . PHP_EOL;
return [
['John', '[email protected]', '1000'],
['Jane', '[email protected]', '2000'],
];
}
protected function format(array $data): string
{
echo "Форматируем в CSV" . PHP_EOL;
$rows = array_map(fn($row) => implode(',', $row), $data);
return implode("\n", $rows);
}
protected function save(string $content): void
{
echo "Сохраняем report.csv" . PHP_EOL;
// file_put_contents('report.csv', $content);
}
}
// HTML отчёт
class HTMLReport extends ReportGenerator
{
protected function fetchData(): array
{
echo "Получаем данные для HTML" . PHP_EOL;
return [
['John', '[email protected]', '1000'],
['Jane', '[email protected]', '2000'],
];
}
protected function format(array $data): string
{
echo "Форматируем в HTML" . PHP_EOL;
$rows = array_map(
fn($row) => "<tr><td>" . implode('</td><td>', $row) . "</td></tr>",
$data
);
return "<table>" . implode('', $rows) . "</table>";
}
protected function save(string $content): void
{
echo "Сохраняем report.html" . PHP_EOL;
// file_put_contents('report.html', $content);
}
// Переопределяем хук
protected function sendNotification(): void
{
echo "Отправляем email — HTML отчёт готов" . PHP_EOL;
}
}
// PDF отчёт
class PDFReport extends ReportGenerator
{
protected function fetchData(): array
{
echo "Получаем данные для PDF" . PHP_EOL;
return [
['John', '[email protected]', '1000'],
];
}
protected function format(array $data): string
{
echo "Форматируем в PDF" . PHP_EOL;
return "PDF content...";
}
protected function save(string $content): void
{
echo "Сохраняем report.pdf" . PHP_EOL;
}
}
// Использование
echo "--- CSV ---" . PHP_EOL;
(new CSVReport())->generate();
echo PHP_EOL . "--- HTML ---" . PHP_EOL;
(new HTMLReport())->generate();
echo PHP_EOL . "--- PDF ---" . PHP_EOL;
(new PDFReport())->generate();
```
---
### Вывод
```
--- CSV ---
Получаем данные для CSV
Форматируем в CSV
Сохраняем report.csv
--- HTML ---
Получаем данные для HTML
Форматируем в HTML
Сохраняем report.html
Отправляем email — HTML отчёт готов
--- PDF ---
Получаем данные для PDF
Форматируем в PDF
Сохраняем report.pdf
Хуки
Хук — это необязательный шаг с пустой дефолтной реализацией. Подкласс может переопределить его, а может оставить как есть:
// Базовый класс
protected function sendNotification(): void
{
// пусто — по умолчанию ничего не делаем
}
// HTMLReport переопределил — отправляет email
// CSVReport и PDFReport не переопределили — ничего не происходит
В абстрактный класс выносишь то что общее для всех наследников. Если переменная нужна только одному наследнику — объявляешь только в нём.
Паттерн Visitor (Посетитель)
Visitor — позволяет добавлять новые операции к объектам не меняя их классы. Выносишь операцию в отдельный класс-посетитель, который приходит к каждому объекту и делает своё дело.
Проблема без Visitor
class Developer
{
public function __construct(
public string $name,
public int $salary,
) {}
}
class Designer
{
public function __construct(
public string $name,
public int $salary,
) {}
}
class Manager
{
public function __construct(
public string $name,
public int $salary,
public int $bonus,
) {}
}
// Хотим добавить новую операцию — подсчёт налогов
// Придётся лезть в каждый класс и добавлять метод
class Developer
{
public function calculateTax(): int { return $this->salary * 0.2; }
}
class Designer
{
public function calculateTax(): int { return $this->salary * 0.2; }
}
class Manager
{
public function calculateTax(): int { return ($this->salary + $this->bonus) * 0.25; }
}
// Добавляешь ещё операцию — снова лезешь во все классы
Решение с Visitor
// Интерфейс посетителя
interface EmployeeVisitor
{
public function visitDeveloper(Developer $developer): mixed;
public function visitDesigner(Designer $designer): mixed;
public function visitManager(Manager $manager): mixed;
}
// Интерфейс элемента — принимает посетителя
interface Employee
{
public function accept(EmployeeVisitor $visitor): mixed;
}
// Классы сотрудников — не знают про операции
class Developer implements Employee
{
public function __construct(
public string $name,
public int $salary,
) {}
public function accept(EmployeeVisitor $visitor): mixed
{
return $visitor->visitDeveloper($this);
}
}
class Designer implements Employee
{
public function __construct(
public string $name,
public int $salary,
) {}
public function accept(EmployeeVisitor $visitor): mixed
{
return $visitor->visitDesigner($this);
}
}
class Manager implements Employee
{
public function __construct(
public string $name,
public int $salary,
public int $bonus,
) {}
public function accept(EmployeeVisitor $visitor): mixed
{
return $visitor->visitManager($this);
}
}
// Посетитель 1 — подсчёт налогов
class TaxVisitor implements EmployeeVisitor
{
public function visitDeveloper(Developer $developer): int
{
$tax = (int)($developer->salary * 0.2);
echo "{$developer->name} налог: {$tax}$" . PHP_EOL;
return $tax;
}
public function visitDesigner(Designer $designer): int
{
$tax = (int)($designer->salary * 0.2);
echo "{$designer->name} налог: {$tax}$" . PHP_EOL;
return $tax;
}
public function visitManager(Manager $manager): int
{
$tax = (int)(($manager->salary + $manager->bonus) * 0.25);
echo "{$manager->name} налог: {$tax}$" . PHP_EOL;
return $tax;
}
}
// Посетитель 2 — отчёт по зарплатам
class SalaryReportVisitor implements EmployeeVisitor
{
public function visitDeveloper(Developer $developer): string
{
$report = "{$developer->name} (Developer): {$developer->salary}$";
echo $report . PHP_EOL;
return $report;
}
public function visitDesigner(Designer $designer): string
{
$report = "{$designer->name} (Designer): {$designer->salary}$";
echo $report . PHP_EOL;
return $report;
}
public function visitManager(Manager $manager): string
{
$total = $manager->salary + $manager->bonus;
$report = "{$manager->name} (Manager): {$manager->salary}$ + {$manager->bonus}$ бонус = {$total}$";
echo $report . PHP_EOL;
return $report;
}
}
// Использование
$employees = [
new Developer('John', 3000),
new Designer('Jane', 2500),
new Manager('Bob', 4000, 1000),
];
echo "--- Налоги ---" . PHP_EOL;
$taxVisitor = new TaxVisitor();
foreach ($employees as $employee) {
$employee->accept($taxVisitor);
}
echo PHP_EOL . "--- Зарплаты ---" . PHP_EOL;
$salaryVisitor = new SalaryReportVisitor();
foreach ($employees as $employee) {
$employee->accept($salaryVisitor);
}
```
---
### Вывод
```
--- Налоги ---
John налог: 600$
Jane налог: 500$
Bob налог: 1250$
--- Зарплаты ---
John (Developer): 3000$
Jane (Designer): 2500$
Bob (Manager): 4000$ + 1000$ бонус = 5000$
Добавляем новую операцию — классы не трогаем
// Просто создаём нового посетителя
class BonusVisitor implements EmployeeVisitor
{
public function visitDeveloper(Developer $developer): int
{
$bonus = (int)($developer->salary * 0.1);
echo "{$developer->name} бонус: {$bonus}$" . PHP_EOL;
return $bonus;
}
public function visitDesigner(Designer $designer): int
{
$bonus = (int)($designer->salary * 0.1);
echo "{$designer->name} бонус: {$bonus}$" . PHP_EOL;
return $bonus;
}
public function visitManager(Manager $manager): int
{
echo "{$manager->name} бонус уже включён" . PHP_EOL;
return $manager->bonus;
}
}
// Developer, Designer, Manager не трогаем
foreach ($employees as $employee) {
$employee->accept(new BonusVisitor());
}