Порождающие паттерны (Creational Patterns)
Порождающие паттерны абстрагируют процесс создания объектов. Вместо прямого new они предоставляют гибкие механизмы инстанцирования.
Singleton
Проблема
Нужно гарантировать, что у класса будет ровно один экземпляр, доступный глобально. Например, подключение к базе данных или конфигурация приложения.
Решение
<?php
declare(strict_types=1);
final class DatabaseConnection
{
private static ?self $instance = null;
// Private constructor prevents external instantiation
private function __construct(
private readonly string $dsn,
) {}
public static function getInstance(string $dsn = 'sqlite::memory:'): self
{
return self::$instance ??= new self($dsn);
}
public function query(string $sql): array
{
// Execute query...
return [];
}
// Prevent cloning
private function __clone(): void {}
// Prevent unserialization
public function __wakeup(): never
{
throw new \RuntimeException('Cannot unserialize singleton');
}
}
// Usage
$db = DatabaseConnection::getInstance('mysql:host=localhost;dbname=app');
$db->query('SELECT * FROM users');
PHP 8.4: Singleton через enum
<?php
declare(strict_types=1);
// Enum is a natural singleton -- only one instance per case
enum AppConfig
{
case Instance;
private const CONFIG_PATH = '/etc/app/config.json';
public function get(string $key): mixed
{
static $config = null;
$config ??= json_decode(
file_get_contents(self::CONFIG_PATH),
true,
512,
JSON_THROW_ON_ERROR,
);
return $config[$key] ?? null;
}
}
// Usage
$dbHost = AppConfig::Instance->get('database.host');
Когда использовать
- Подключение к внешнему ресурсу (БД, кеш) -- но лучше через DI
- Конфигурация приложения
- Логирование
В Symfony
В Symfony Singleton не нужен напрямую -- DI-контейнер делает это за вас:
# services.yaml
services:
App\Service\DatabaseConnection:
shared: true # default — already singleton per container
<?php
declare(strict_types=1);
// Symfony manages single instance automatically
final readonly class OrderService
{
public function __construct(
private DatabaseConnection $db, // Same instance everywhere
) {}
}
Важно: В современном PHP Singleton как паттерн почти всегда заменяется Dependency Injection. Контейнер сам гарантирует единственный экземпляр.
Factory Method
Проблема
Код создаёт объекты через new, но конкретный класс должен определяться в рантайме. Прямое использование new привязывает к конкретной реализации.
Решение
<?php
declare(strict_types=1);
// Product interface
interface Notification
{
public function send(string $recipient, string $message): void;
}
// Concrete products
final readonly class EmailNotification implements Notification
{
public function send(string $recipient, string $message): void
{
// Send email via SMTP
echo "Email to {$recipient}: {$message}\n";
}
}
final readonly class SmsNotification implements Notification
{
public function send(string $recipient, string $message): void
{
// Send SMS via gateway
echo "SMS to {$recipient}: {$message}\n";
}
}
final readonly class TelegramNotification implements Notification
{
public function send(string $recipient, string $message): void
{
// Send via Telegram Bot API
echo "Telegram to {$recipient}: {$message}\n";
}
}
// Creator with factory method
abstract class NotificationFactory
{
// Factory method -- subclasses decide which class to instantiate
abstract protected function createNotification(): Notification;
// Template method using factory
public function notify(string $recipient, string $message): void
{
$notification = $this->createNotification();
$notification->send($recipient, $message);
}
}
final class EmailNotificationFactory extends NotificationFactory
{
protected function createNotification(): Notification
{
return new EmailNotification();
}
}
final class SmsNotificationFactory extends NotificationFactory
{
protected function createNotification(): Notification
{
return new SmsNotification();
}
}
// Usage
$factory = new EmailNotificationFactory();
$factory->notify('[email protected]', 'Hello!');
PHP 8.4: Factory с enum и new без скобок
<?php
declare(strict_types=1);
enum NotificationType: string
{
case Email = 'email';
case Sms = 'sms';
case Telegram = 'telegram';
public function create(): Notification
{
return match ($this) {
self::Email => new EmailNotification,
self::Sms => new SmsNotification,
self::Telegram => new TelegramNotification,
};
}
}
// Usage: clean and type-safe
$type = NotificationType::from('email');
$notification = $type->create();
$notification->send('[email protected]', 'Welcome!');
Когда использовать
- Тип объекта определяется в рантайме (из конфига, БД, API)
- Нужна расширяемость -- новые типы без изменения существующего кода
- Инкапсуляция логики создания
В Symfony
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Psr\Container\ContainerInterface;
// Symfony service locator as factory
final readonly class NotificationFactory
{
public function __construct(
#[AutowireLocator(services: [
'email' => EmailNotification::class,
'sms' => SmsNotification::class,
'telegram' => TelegramNotification::class,
])]
private ContainerInterface $notifications,
) {}
public function create(string $type): Notification
{
if (!$this->notifications->has($type)) {
throw new \InvalidArgumentException("Unknown notification type: {$type}");
}
return $this->notifications->get($type);
}
}
Abstract Factory
Проблема
Нужно создавать семейства связанных объектов без привязки к их конкретным классам. Например, элементы UI для разных ОС или отчёты для разных форматов.
Решение
<?php
declare(strict_types=1);
// Abstract products
interface Button
{
public function render(): string;
}
interface Input
{
public function render(): string;
}
// Concrete products: Bootstrap family
final readonly class BootstrapButton implements Button
{
public function __construct(
private string $label,
) {}
public function render(): string
{
return "<button class=\"btn btn-primary\">{$this->label}</button>";
}
}
final readonly class BootstrapInput implements Input
{
public function __construct(
private string $placeholder,
) {}
public function render(): string
{
return "<input class=\"form-control\" placeholder=\"{$this->placeholder}\">";
}
}
// Concrete products: Tailwind family
final readonly class TailwindButton implements Button
{
public function __construct(
private string $label,
) {}
public function render(): string
{
return "<button class=\"px-4 py-2 bg-blue-500 text-white rounded\">{$this->label}</button>";
}
}
final readonly class TailwindInput implements Input
{
public function __construct(
private string $placeholder,
) {}
public function render(): string
{
return "<input class=\"border rounded px-3 py-2\" placeholder=\"{$this->placeholder}\">";
}
}
// Abstract factory
interface UiFactory
{
public function createButton(string $label): Button;
public function createInput(string $placeholder): Input;
}
// Concrete factories
final class BootstrapUiFactory implements UiFactory
{
public function createButton(string $label): Button
{
return new BootstrapButton($label);
}
public function createInput(string $placeholder): Input
{
return new BootstrapInput($placeholder);
}
}
final class TailwindUiFactory implements UiFactory
{
public function createButton(string $label): Button
{
return new TailwindButton($label);
}
public function createInput(string $placeholder): Input
{
return new TailwindInput($placeholder);
}
}
// Client code works with any family
function renderForm(UiFactory $factory): string
{
$button = $factory->createButton('Submit');
$input = $factory->createInput('Enter name...');
return $input->render() . "\n" . $button->render();
}
// Switch entire UI family with one change
echo renderForm(new TailwindUiFactory());
Когда использовать
- Система должна работать с семействами связанных объектов
- Нужно гарантировать совместимость объектов одного семейства
- Хотите скрыть детали реализации от клиентского кода
В Symfony
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\When;
// Register concrete factory based on environment
#[When(env: 'dev')]
#[AsAlias(UiFactory::class)]
final class BootstrapUiFactoryService extends BootstrapUiFactory {}
#[When(env: 'prod')]
#[AsAlias(UiFactory::class)]
final class TailwindUiFactoryService extends TailwindUiFactory {}
Builder
Проблема
Объект имеет множество параметров. Конструктор с 10+ аргументами нечитаем. Разные комбинации параметров порождают телескопический конструктор.
Решение
<?php
declare(strict_types=1);
// Complex product
final readonly class Query
{
public function __construct(
public string $table,
public array $select,
public array $where,
public ?string $orderBy,
public ?string $direction,
public ?int $limit,
public ?int $offset,
) {}
public function toSql(): string
{
$columns = implode(', ', $this->select) ?: '*';
$sql = "SELECT {$columns} FROM {$this->table}";
if ($this->where !== []) {
$conditions = implode(' AND ', $this->where);
$sql .= " WHERE {$conditions}";
}
if ($this->orderBy !== null) {
$sql .= " ORDER BY {$this->orderBy} {$this->direction}";
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
return $sql;
}
}
// Builder
final class QueryBuilder
{
private string $table = '';
private array $select = [];
private array $where = [];
private ?string $orderBy = null;
private string $direction = 'ASC';
private ?int $limit = null;
private ?int $offset = null;
public function from(string $table): self
{
$this->table = $table;
return $this;
}
public function select(string ...$columns): self
{
$this->select = $columns;
return $this;
}
public function where(string $condition): self
{
$this->where[] = $condition;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->orderBy = $column;
$this->direction = $direction;
return $this;
}
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function offset(int $offset): self
{
$this->offset = $offset;
return $this;
}
public function build(): Query
{
if ($this->table === '') {
throw new \LogicException('Table name is required');
}
return new Query(
table: $this->table,
select: $this->select,
where: $this->where,
orderBy: $this->orderBy,
direction: $this->direction,
limit: $this->limit,
offset: $this->offset,
);
}
}
// Usage: fluent interface
$query = (new QueryBuilder())
->from('users')
->select('id', 'name', 'email')
->where('active = 1')
->where('age > 18')
->orderBy('name')
->limit(10)
->build();
echo $query->toSql();
// SELECT id, name, email FROM users WHERE active = 1 AND age > 18 ORDER BY name ASC LIMIT 10
PHP 8.4: Builder с property hooks
<?php
declare(strict_types=1);
final class HttpRequestBuilder
{
private string $method = 'GET';
private string $url = '';
private array $headers = [];
private ?string $body = null;
// Property hook for validation on set
public string $timeout {
set(string $value) {
if ((int) $value < 0) {
throw new \InvalidArgumentException('Timeout must be positive');
}
$this->timeout = $value;
}
}
public function method(string $method): self
{
$this->method = strtoupper($method);
return $this;
}
public function url(string $url): self
{
$this->url = $url;
return $this;
}
public function header(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
public function body(string $body): self
{
$this->body = $body;
return $this;
}
public function build(): HttpRequest
{
return new HttpRequest(
method: $this->method,
url: $this->url,
headers: $this->headers,
body: $this->body,
);
}
}
final readonly class HttpRequest
{
public function __construct(
public string $method,
public string $url,
public array $headers,
public ?string $body,
) {}
}
Когда использовать
- Объект с множеством необязательных параметров
- Нужен пошаговый процесс создания
- Один и тот же процесс создания должен давать разные представления
В Symfony
<?php
declare(strict_types=1);
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
// Symfony FormBuilder -- classic Builder pattern
final class UserFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['label' => 'Name'])
->add('email', EmailType::class, ['label' => 'Email'])
->add('save', SubmitType::class, ['label' => 'Register']);
}
}
Prototype
Проблема
Нужно создавать копии объектов, не зная их конкретных классов. Или создание нового объекта дорого (запрос к БД, API), а нужно много похожих.
Решение
<?php
declare(strict_types=1);
// Prototype interface
interface Prototype
{
public function clone(): static;
}
final class Report implements Prototype
{
/** @var list<string> */
private array $sections = [];
public function __construct(
private string $title,
private string $template,
private \DateTimeImmutable $date,
) {}
public function addSection(string $section): void
{
$this->sections[] = $section;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function clone(): static
{
// Deep copy with new date
$clone = clone $this;
$clone->date = new \DateTimeImmutable();
return $clone;
}
public function describe(): string
{
$sectionCount = count($this->sections);
return "{$this->title} ({$this->template}) - {$sectionCount} sections";
}
}
// Create expensive prototype once
$monthlyReport = new Report(
title: 'Monthly Sales Report',
template: 'financial',
date: new \DateTimeImmutable(),
);
$monthlyReport->addSection('Revenue');
$monthlyReport->addSection('Expenses');
$monthlyReport->addSection('Profit');
// Clone and customize -- much cheaper
$januaryReport = $monthlyReport->clone();
$januaryReport->setTitle('January Sales Report');
$februaryReport = $monthlyReport->clone();
$februaryReport->setTitle('February Sales Report');
PHP 8.4: Clone с readonly и clone with
<?php
declare(strict_types=1);
// In PHP 8.4 readonly properties can be reset in clone context
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): self
{
// PHP 8.4: clone with expression (RFC accepted)
$clone = clone $this;
// Readonly can be written in __clone context
return $clone;
}
public function multiply(int $factor): self
{
return new self(
amount: $this->amount * $factor,
currency: $this->currency,
);
}
}
// Immutable value object with easy cloning
$price = new Money(amount: 1000, currency: 'USD');
$doubled = $price->multiply(2);
echo $doubled->amount; // 2000
Когда использовать
- Создание объекта дорого (сложная инициализация, запросы)
- Нужны вариации одного объекта-шаблона
- Конкретный тип объекта неизвестен клиенту
В Symfony
<?php
declare(strict_types=1);
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;
// Symfony Messenger: Envelope is effectively a prototype
$baseEnvelope = new Envelope(new OrderCreatedEvent(orderId: 1));
// Clone and add stamps for different delivery configurations
$delayedEnvelope = $baseEnvelope->with(new DelayStamp(5000));
Сравнение порождающих паттернов
| Паттерн | Ключевая идея | PHP 8.4 подход |
|---|---|---|
| Singleton | Один экземпляр | Enum case или DI container |
| Factory Method | Делегирование создания | Enum с create() |
| Abstract Factory | Семейство объектов | Interface + DI |
| Builder | Пошаговое создание | Fluent interface + named args |
| Prototype | Клонирование | clone + readonly |