MidТеория13 min

Структурные паттерны

Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight на PHP 8.4

Структурные паттерны (Structural Patterns)

Структурные паттерны отвечают за композицию классов и объектов в более крупные структуры, сохраняя гибкость и эффективность.

Adapter

Проблема

Есть класс с нужной функциональностью, но его интерфейс несовместим с остальным кодом. Переписать класс нельзя -- это внешняя библиотека или легаси-код.

Решение

<?php
declare(strict_types=1);

// Target interface our code expects
interface PaymentGateway
{
    public function charge(float $amount, string $currency): PaymentResult;
}

// Result DTO
final readonly class PaymentResult
{
    public function __construct(
        public bool $success,
        public string $transactionId,
        public ?string $error = null,
    ) {}
}

// External SDK with incompatible interface (can't modify)
final class StripeApiClient
{
    public function createCharge(int $amountInCents, string $cur, string $source): array
    {
        // External API call to Stripe...
        return ['id' => 'ch_123abc', 'status' => 'succeeded'];
    }
}

// Adapter wraps the incompatible class
final readonly class StripePaymentAdapter implements PaymentGateway
{
    public function __construct(
        private StripeApiClient $stripe,
        private string $defaultSource = 'tok_visa',
    ) {}

    public function charge(float $amount, string $currency): PaymentResult
    {
        $amountInCents = (int) round($amount * 100);

        try {
            $result = $this->stripe->createCharge(
                amountInCents: $amountInCents,
                cur: strtolower($currency),
                source: $this->defaultSource,
            );

            return new PaymentResult(
                success: $result['status'] === 'succeeded',
                transactionId: $result['id'],
            );
        } catch (\Throwable $e) {
            return new PaymentResult(
                success: false,
                transactionId: '',
                error: $e->getMessage(),
            );
        }
    }
}

// Client code works with unified interface
function processOrder(PaymentGateway $gateway, float $total): void
{
    $result = $gateway->charge($total, 'USD');
    if ($result->success) {
        echo "Payment successful: {$result->transactionId}\n";
    }
}

Когда использовать

  • Интеграция сторонних библиотек с несовместимым интерфейсом
  • Унификация нескольких разных API под одним интерфейсом
  • Работа с легаси-кодом без его переписывания

В Symfony

<?php
declare(strict_types=1);

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

// Symfony Serializer normalizers -- adapters for different data formats
final class LegacyUserNormalizer implements NormalizerInterface
{
    public function normalize(
        mixed $data,
        ?string $format = null,
        array $context = [],
    ): array {
        /** @var LegacyUser $data */
        // Adapt legacy user format to modern API format
        return [
            'id' => $data->getUserId(),
            'full_name' => $data->getFirstName() . ' ' . $data->getLastName(),
            'email' => $data->getElectronicMail(),
        ];
    }

    public function supportsNormalization(
        mixed $data,
        ?string $format = null,
        array $context = [],
    ): bool {
        return $data instanceof LegacyUser;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [LegacyUser::class => true];
    }
}

Decorator

Проблема

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

Решение

<?php
declare(strict_types=1);

// Component interface
interface Logger
{
    public function log(string $level, string $message): void;
}

// Concrete component
final class FileLogger implements Logger
{
    public function __construct(
        private readonly string $path,
    ) {}

    public function log(string $level, string $message): void
    {
        $line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), $level, $message);
        file_put_contents($this->path, $line, FILE_APPEND);
    }
}

// Base decorator
abstract class LoggerDecorator implements Logger
{
    public function __construct(
        protected readonly Logger $inner,
    ) {}
}

// Concrete decorator: adds timestamp prefix
final class TimestampLogger extends LoggerDecorator
{
    public function log(string $level, string $message): void
    {
        $message = '[' . microtime(true) . '] ' . $message;
        $this->inner->log($level, $message);
    }
}

// Concrete decorator: filters by level
final class MinLevelLogger extends LoggerDecorator
{
    private const LEVELS = ['debug' => 0, 'info' => 1, 'warning' => 2, 'error' => 3];

    public function __construct(
        Logger $inner,
        private readonly string $minLevel = 'info',
    ) {
        parent::__construct($inner);
    }

    public function log(string $level, string $message): void
    {
        if ((self::LEVELS[$level] ?? 0) >= (self::LEVELS[$this->minLevel] ?? 0)) {
            $this->inner->log($level, $message);
        }
    }
}

// Concrete decorator: adds context info
final class ContextLogger extends LoggerDecorator
{
    public function __construct(
        Logger $inner,
        private readonly string $context,
    ) {
        parent::__construct($inner);
    }

    public function log(string $level, string $message): void
    {
        $this->inner->log($level, "[{$this->context}] {$message}");
    }
}

// Stack decorators in any combination
$logger = new ContextLogger(
    inner: new MinLevelLogger(
        inner: new TimestampLogger(
            inner: new FileLogger('/var/log/app.log'),
        ),
        minLevel: 'warning',
    ),
    context: 'OrderService',
);

$logger->log('debug', 'Skip this');       // Filtered out
$logger->log('error', 'Payment failed');  // Written with context + timestamp

Когда использовать

  • Динамическое добавление/удаление обязанностей объекта
  • Невозможность использовать наследование (final класс)
  • Комбинации поведений без взрыва подклассов

В Symfony

<?php
declare(strict_types=1);

use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;

// Symfony native decorator support
#[AsDecorator(decorates: Logger::class)]
final class CachingLogger implements Logger
{
    private array $cache = [];

    public function __construct(
        #[AutowireDecorated]
        private readonly Logger $inner,
    ) {}

    public function log(string $level, string $message): void
    {
        $key = md5($level . $message);
        if (!isset($this->cache[$key])) {
            $this->inner->log($level, $message);
            $this->cache[$key] = true;
        }
    }
}

Facade

Проблема

Подсистема состоит из множества классов с множеством методов. Клиентский код не должен знать детали и управлять ими напрямую.

Решение

<?php
declare(strict_types=1);

// Complex subsystem classes
final class InventoryService
{
    public function checkStock(int $productId, int $quantity): bool
    {
        // Check warehouse...
        return true;
    }

    public function reserve(int $productId, int $quantity): string
    {
        return 'RSV-' . uniqid();
    }
}

final class PaymentService
{
    public function authorize(float $amount, string $cardToken): string
    {
        return 'AUTH-' . uniqid();
    }

    public function capture(string $authorizationId): bool
    {
        return true;
    }
}

final class ShippingService
{
    public function calculateCost(string $address, float $weight): float
    {
        return 9.99;
    }

    public function createShipment(string $address, string $reservationId): string
    {
        return 'SHIP-' . uniqid();
    }
}

final class NotificationService
{
    public function sendOrderConfirmation(string $email, string $orderId): void
    {
        // Send email...
    }
}

// Facade simplifies interaction with subsystem
final readonly class OrderFacade
{
    public function __construct(
        private InventoryService $inventory,
        private PaymentService $payment,
        private ShippingService $shipping,
        private NotificationService $notification,
    ) {}

    /**
     * One method to place an order -- hides subsystem complexity
     */
    public function placeOrder(
        int $productId,
        int $quantity,
        float $price,
        string $cardToken,
        string $address,
        string $email,
    ): OrderConfirmation {
        // Step 1: Check and reserve inventory
        if (!$this->inventory->checkStock($productId, $quantity)) {
            throw new \RuntimeException('Product out of stock');
        }
        $reservationId = $this->inventory->reserve($productId, $quantity);

        // Step 2: Calculate total with shipping
        $shippingCost = $this->shipping->calculateCost($address, weight: 1.5);
        $total = ($price * $quantity) + $shippingCost;

        // Step 3: Process payment
        $authId = $this->payment->authorize($total, $cardToken);
        $this->payment->capture($authId);

        // Step 4: Create shipment
        $shipmentId = $this->shipping->createShipment($address, $reservationId);

        // Step 5: Notify customer
        $orderId = 'ORD-' . uniqid();
        $this->notification->sendOrderConfirmation($email, $orderId);

        return new OrderConfirmation(
            orderId: $orderId,
            shipmentId: $shipmentId,
            total: $total,
        );
    }
}

final readonly class OrderConfirmation
{
    public function __construct(
        public string $orderId,
        public string $shipmentId,
        public float $total,
    ) {}
}

// Client code: ONE call instead of managing 4 services
$facade = new OrderFacade(
    new InventoryService(),
    new PaymentService(),
    new ShippingService(),
    new NotificationService(),
);

$confirmation = $facade->placeOrder(
    productId: 42,
    quantity: 2,
    price: 29.99,
    cardToken: 'tok_visa',
    address: '123 Main St',
    email: '[email protected]',
);

Когда использовать

  • Нужен простой интерфейс к сложной подсистеме
  • Слишком много зависимостей между клиентом и подсистемой
  • Хотите разделить подсистему на слои

В Symfony

<?php
declare(strict_types=1);

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

// Symfony Mailer -- facade over transport, formatting, queuing
final readonly class NotificationFacade
{
    public function __construct(
        private MailerInterface $mailer, // Hides SMTP, transport, etc.
    ) {}

    public function notifyOrderShipped(string $to, string $orderId): void
    {
        $email = (new Email())
            ->from('[email protected]')
            ->to($to)
            ->subject("Order #{$orderId} shipped!")
            ->text("Your order #{$orderId} is on its way.");

        $this->mailer->send($email);
    }
}

Proxy

Проблема

Нужно контролировать доступ к объекту: добавить ленивую загрузку, кеширование, логирование или проверку прав -- без изменения самого объекта.

Решение

<?php
declare(strict_types=1);

// Subject interface
interface UserRepository
{
    public function find(int $id): ?UserDto;
    public function findAll(): array;
}

final readonly class UserDto
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
    ) {}
}

// Real subject -- expensive database calls
final readonly class DatabaseUserRepository implements UserRepository
{
    public function find(int $id): ?UserDto
    {
        // Heavy database query
        return new UserDto(id: $id, name: 'John', email: '[email protected]');
    }

    public function findAll(): array
    {
        // Heavy query returning thousands of rows
        return [];
    }
}

// Caching proxy
final class CachingUserRepository implements UserRepository
{
    /** @var array<int, UserDto> */
    private array $cache = [];

    public function __construct(
        private readonly UserRepository $real,
    ) {}

    public function find(int $id): ?UserDto
    {
        if (!isset($this->cache[$id])) {
            $this->cache[$id] = $this->real->find($id);
        }

        return $this->cache[$id];
    }

    public function findAll(): array
    {
        return $this->real->findAll();
    }
}

// Logging proxy
final readonly class LoggingUserRepository implements UserRepository
{
    public function __construct(
        private UserRepository $real,
        private Logger $logger,
    ) {}

    public function find(int $id): ?UserDto
    {
        $this->logger->log('info', "Finding user #{$id}");
        $start = microtime(true);

        $result = $this->real->find($id);

        $duration = round((microtime(true) - $start) * 1000, 2);
        $this->logger->log('info', "Found user #{$id} in {$duration}ms");

        return $result;
    }

    public function findAll(): array
    {
        $this->logger->log('info', 'Finding all users');
        return $this->real->findAll();
    }
}

// Stack proxies
$repository = new LoggingUserRepository(
    real: new CachingUserRepository(
        real: new DatabaseUserRepository(),
    ),
    logger: new FileLogger('/var/log/app.log'),
);

$user = $repository->find(1); // Logged + cached
$user = $repository->find(1); // Logged + from cache (no DB hit)

PHP 8.4: Lazy proxy с ghost objects

<?php
declare(strict_types=1);

// PHP 8.4 lazy objects (built-in lazy proxy support)
final class HeavyReport
{
    public readonly string $data;

    public function __construct(int $reportId)
    {
        // Expensive initialization
        sleep(2);
        $this->data = "Report #{$reportId} data loaded";
    }
}

// PHP 8.4 lazy ghost proxy via ReflectionClass
$reflector = new \ReflectionClass(HeavyReport::class);
$lazyReport = $reflector->newLazyGhost(
    function (HeavyReport $report): void {
        // Called only when property is first accessed
        $report->__construct(42);
    },
);

// Object exists but NOT yet initialized
echo $lazyReport instanceof HeavyReport; // true

// Initialization happens HERE on first property access
echo $lazyReport->data; // "Report #42 data loaded"

Когда использовать

  • Ленивая инициализация тяжёлых объектов
  • Кеширование результатов
  • Контроль доступа (проверка прав)
  • Логирование вызовов

В Symfony

<?php
declare(strict_types=1);

// Doctrine uses proxy for lazy loading entities
// When you access $order->getCustomer(), Doctrine creates a proxy
// that loads the customer from DB only when you access its properties

// Symfony services can be lazy too:
// services.yaml
// App\Service\HeavyService:
//     lazy: true  # Creates a proxy, initialized on first method call

Composite

Проблема

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

Решение

<?php
declare(strict_types=1);

// Component interface
interface MenuComponent
{
    public function render(int $depth = 0): string;
    public function getPrice(): float;
}

// Leaf
final readonly class MenuItem implements MenuComponent
{
    public function __construct(
        private string $name,
        private float $price,
    ) {}

    public function render(int $depth = 0): string
    {
        $indent = str_repeat('  ', $depth);
        return "{$indent}- {$this->name}: \${$this->price}\n";
    }

    public function getPrice(): float
    {
        return $this->price;
    }
}

// Composite
final class MenuCategory implements MenuComponent
{
    /** @var list<MenuComponent> */
    private array $children = [];

    public function __construct(
        private readonly string $name,
    ) {}

    public function add(MenuComponent $component): self
    {
        $this->children[] = $component;
        return $this;
    }

    public function render(int $depth = 0): string
    {
        $indent = str_repeat('  ', $depth);
        $output = "{$indent}[{$this->name}]\n";

        foreach ($this->children as $child) {
            $output .= $child->render($depth + 1);
        }

        return $output;
    }

    public function getPrice(): float
    {
        return array_sum(
            array_map(
                fn(MenuComponent $c): float => $c->getPrice(),
                $this->children,
            ),
        );
    }
}

// Build tree structure
$menu = new MenuCategory('Restaurant Menu');

$appetizers = new MenuCategory('Appetizers');
$appetizers->add(new MenuItem('Soup', 5.99));
$appetizers->add(new MenuItem('Salad', 7.99));

$mains = new MenuCategory('Main Courses');
$mains->add(new MenuItem('Steak', 24.99));
$mains->add(new MenuItem('Pasta', 16.99));

$menu->add($appetizers);
$menu->add($mains);

// Uniform access: render whole tree or any branch
echo $menu->render();
echo "Total: \$" . $menu->getPrice(); // Sum of all items

Когда использовать

  • Древовидная структура данных (меню, каталоги, организации)
  • Единообразная обработка листьев и контейнеров
  • Рекурсивная композиция объектов

В Symfony

<?php
declare(strict_types=1);

use Symfony\Component\Form\FormInterface;

// Symfony Form -- classic Composite pattern
// A form can contain fields (leaves) and sub-forms (composites)
// $form->isValid() recursively validates all children

// Symfony Finder -- composite for file system traversal
use Symfony\Component\Finder\Finder;

$finder = new Finder();
$finder->files()->in('/src')->name('*.php');

foreach ($finder as $file) {
    echo $file->getRealPath() . "\n";
}

Bridge

Проблема

Нужно разделить абстракцию и реализацию, чтобы они могли изменяться независимо. Без Bridge каждая комбинация абстракция + реализация требует отдельного класса.

Решение

<?php
declare(strict_types=1);

// Implementation interface (how to render)
interface Renderer
{
    public function renderTitle(string $title): string;
    public function renderParagraph(string $text): string;
    public function renderImage(string $url, string $alt): string;
}

final class HtmlRenderer implements Renderer
{
    public function renderTitle(string $title): string
    {
        return "<h1>{$title}</h1>";
    }

    public function renderParagraph(string $text): string
    {
        return "<p>{$text}</p>";
    }

    public function renderImage(string $url, string $alt): string
    {
        return "<img src=\"{$url}\" alt=\"{$alt}\">";
    }
}

final class MarkdownRenderer implements Renderer
{
    public function renderTitle(string $title): string
    {
        return "# {$title}";
    }

    public function renderParagraph(string $text): string
    {
        return "\n{$text}\n";
    }

    public function renderImage(string $url, string $alt): string
    {
        return "![{$alt}]({$url})";
    }
}

// Abstraction (what to render)
abstract class Page
{
    public function __construct(
        protected readonly Renderer $renderer,
    ) {}

    abstract public function render(): string;
}

// Refined abstractions
final class ArticlePage extends Page
{
    public function __construct(
        Renderer $renderer,
        private readonly string $title,
        private readonly string $body,
        private readonly ?string $imageUrl = null,
    ) {
        parent::__construct($renderer);
    }

    public function render(): string
    {
        $output = $this->renderer->renderTitle($this->title);
        $output .= "\n" . $this->renderer->renderParagraph($this->body);

        if ($this->imageUrl !== null) {
            $output .= "\n" . $this->renderer->renderImage($this->imageUrl, $this->title);
        }

        return $output;
    }
}

final class ProductPage extends Page
{
    public function __construct(
        Renderer $renderer,
        private readonly string $name,
        private readonly float $price,
        private readonly string $description,
    ) {
        parent::__construct($renderer);
    }

    public function render(): string
    {
        $output = $this->renderer->renderTitle($this->name);
        $output .= "\n" . $this->renderer->renderParagraph("Price: \${$this->price}");
        $output .= "\n" . $this->renderer->renderParagraph($this->description);

        return $output;
    }
}

// Any page type + any renderer without class explosion
$htmlArticle = new ArticlePage(new HtmlRenderer(), 'Title', 'Body text');
$mdArticle = new ArticlePage(new MarkdownRenderer(), 'Title', 'Body text');

echo $htmlArticle->render(); // <h1>Title</h1><p>Body text</p>
echo $mdArticle->render();   // # Title\nBody text

Когда использовать

  • Множество вариаций в двух измерениях (тип + реализация)
  • Нужно менять реализацию в рантайме
  • Хотите избежать взрыва наследования

В Symfony

<?php
declare(strict_types=1);

// Symfony Cache: abstraction (CacheInterface) + implementation (adapter)
// The "bridge" allows switching between Redis, Memcached, Filesystem
// without changing business code

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

final readonly class ProductService
{
    public function __construct(
        private CacheInterface $cache, // Abstraction -- doesn't know the adapter
    ) {}

    public function getProduct(int $id): array
    {
        return $this->cache->get("product_{$id}", function (ItemInterface $item): array {
            $item->expiresAfter(3600);
            // Load from database...
            return ['id' => 1, 'name' => 'Widget'];
        });
    }
}

Flyweight

Проблема

Приложению нужно создавать тысячи или миллионы объектов, каждый из которых содержит повторяющиеся данные. Например, текстовый редактор хранит объект для каждого символа в документе -- шрифт, размер, цвет, начертание. Документ из 100 000 символов при 20 уникальных стилях будет хранить 100 000 объектов с дублирующимся состоянием, что съедает всю оперативную память.

Решение

Разделяем состояние объекта на внутреннее (intrinsic -- общее, разделяемое) и внешнее (extrinsic -- уникальное для каждого контекста). Flyweight хранит только внутреннее состояние и переиспользуется через фабрику с пулом.

<?php
declare(strict_types=1);

// Intrinsic state: shared between all characters with the same style
final readonly class GlyphStyle
{
    public function __construct(
        public string $fontFamily,
        public int $fontSize,
        public string $color,
        public bool $bold,
        public bool $italic,
    ) {}

    public function getKey(): string
    {
        $flags = ($this->bold ? 'B' : '') . ($this->italic ? 'I' : '');

        return "{$this->fontFamily}:{$this->fontSize}:{$this->color}:{$flags}";
    }
}

// Flyweight factory: ensures shared instances
final class GlyphStyleFactory
{
    /** @var array<string, GlyphStyle> */
    private array $pool = [];

    public function getStyle(
        string $fontFamily,
        int $fontSize,
        string $color,
        bool $bold = false,
        bool $italic = false,
    ): GlyphStyle {
        $style = new GlyphStyle($fontFamily, $fontSize, $color, $bold, $italic);
        $key = $style->getKey();

        if (!isset($this->pool[$key])) {
            $this->pool[$key] = $style;
        }

        return $this->pool[$key];
    }

    public function getPoolSize(): int
    {
        return count($this->pool);
    }
}

// Extrinsic state: unique per character (position, character itself)
final readonly class Glyph
{
    public function __construct(
        public string $character,
        public int $row,
        public int $col,
        public GlyphStyle $style, // Shared flyweight reference
    ) {}

    public function render(): string
    {
        $weight = $this->style->bold ? 'bold' : 'normal';
        $fontStyle = $this->style->italic ? 'italic' : 'normal';

        return sprintf(
            '<span style="font-family:%s;font-size:%dpx;color:%s;font-weight:%s;font-style:%s">%s</span>',
            $this->style->fontFamily,
            $this->style->fontSize,
            $this->style->color,
            $weight,
            $fontStyle,
            htmlspecialchars($this->character),
        );
    }
}

// Text editor document using flyweight styles
final class Document
{
    /** @var list<Glyph> */
    private array $glyphs = [];

    public function __construct(
        private readonly GlyphStyleFactory $styleFactory,
    ) {}

    public function addCharacter(
        string $char,
        int $row,
        int $col,
        string $fontFamily,
        int $fontSize,
        string $color,
        bool $bold = false,
        bool $italic = false,
    ): void {
        // Factory returns shared style instance
        $style = $this->styleFactory->getStyle($fontFamily, $fontSize, $color, $bold, $italic);

        $this->glyphs[] = new Glyph(
            character: $char,
            row: $row,
            col: $col,
            style: $style,
        );
    }

    public function getGlyphCount(): int
    {
        return count($this->glyphs);
    }

    public function getUniqueStyleCount(): int
    {
        return $this->styleFactory->getPoolSize();
    }
}

// Client code: 10 000 characters but only a few shared styles
$factory = new GlyphStyleFactory();
$document = new Document($factory);

$text = str_repeat('Lorem ipsum dolor sit amet. ', 400); // ~10 000 chars
$col = 0;
$row = 0;

foreach (mb_str_split($text) as $char) {
    // Alternate between 3 styles -- only 3 GlyphStyle objects created
    $style = match (true) {
        $char === '.' => ['Arial', 12, '#999999', false, false],
        ctype_upper($char) => ['Arial', 14, '#000000', true, false],
        default => ['Arial', 12, '#333333', false, false],
    };

    $document->addCharacter($char, $row, $col, ...$style);

    $col++;
    if ($char === "\n" || $col > 80) {
        $row++;
        $col = 0;
    }
}

echo "Characters: {$document->getGlyphCount()}\n";      // ~10 000
echo "Unique styles: {$document->getUniqueStyleCount()}\n"; // 3

// Without Flyweight: 10 000 style objects (~640 KB)
// With Flyweight: 3 style objects (~0.2 KB) -- memory savings ~99.9%

Когда использовать

  • Приложение создаёт огромное количество объектов с повторяющимися данными
  • Большая часть состояния объекта может быть вынесена как внешнее (extrinsic)
  • Объекты можно группировать по общему внутреннему состоянию (intrinsic)
  • Приложение не зависит от идентичности объектов (flyweight-объекты разделяются)

В Symfony

<?php
declare(strict_types=1);

// Symfony Twig: скомпилированные шаблоны кешируются и переиспользуются
// как flyweight-объекты -- один скомпилированный шаблон на множество рендеров

// Symfony Form: FormType создаётся один раз и переиспользуется
// для множества форм одного типа -- внутреннее состояние (конфигурация)
// разделяется, внешнее (данные) уникально для каждой формы

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// FormType -- flyweight: конфигурация полей общая,
// данные формы -- внешнее состояние
final class AddressType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // This configuration is created once and reused
        $builder
            ->add('street', TextType::class)
            ->add('city', TextType::class)
            ->add('zipCode', TextType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => AddressDto::class,
        ]);
    }
}

// Symfony DI: shared services (по умолчанию singleton)
// -- каждый сервис создаётся один раз и инжектируется во все зависимости.
// Это фактически Flyweight на уровне контейнера.

// services.yaml:
//   App\Service\CurrencyConverter:
//     shared: true  # default -- one instance reused everywhere

Сравнение структурных паттернов

Паттерн Ключевая идея Типичный случай
Adapter Совмещение интерфейсов Интеграция SDK
Decorator Добавление поведения Логирование, кеш
Facade Упрощение доступа Сложные подсистемы
Proxy Контроль доступа Lazy loading, кеш
Composite Древовидные структуры Меню, формы
Bridge Разделение абстракция/реализация Рендеринг, хранение
Flyweight Разделение общего состояния Кеш объектов

Проверь себя

5 из 6

Компонент Symfony Form реализует паттерн...

Какой паттерн Symfony использует для lazy loading сущностей Doctrine?

Какую проблему решает паттерн Flyweight?

Какой паттерн лучше всего подходит для интеграции стороннего API с несовместимым интерфейсом?

Чем Decorator отличается от Proxy?