MidТеория5 min

Обзор компонентов

Clock, EventDispatcher, Process, PropertyAccess, Runtime, Lock, deployment, web profiler

Clock Component

Symfony 8.0: Компонент Clock явно включён в темы экзамена.

Clock предоставляет абстракцию времени для тестируемого кода. Вместо new \DateTimeImmutable() используйте ClockInterface.

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\Clock\ClockInterface;

final class SubscriptionManager
{
    public function __construct(
        private readonly ClockInterface $clock,
    ) {
    }

    public function isExpired(Subscription $subscription): bool
    {
        return $subscription->getExpiresAt() < $this->clock->now();
    }

    public function extend(Subscription $subscription, int $days): void
    {
        $newExpiry = $this->clock->now()->modify("+{$days} days");
        $subscription->setExpiresAt($newExpiry);
    }
}

Тестирование с MockClock

<?php

declare(strict_types=1);

namespace App\Tests;

use Symfony\Component\Clock\MockClock;
use PHPUnit\Framework\TestCase;

final class SubscriptionManagerTest extends TestCase
{
    public function testIsExpired(): void
    {
        $clock = new MockClock('2025-06-15 12:00:00');
        $manager = new SubscriptionManager($clock);

        $subscription = new Subscription();
        $subscription->setExpiresAt(new \DateTimeImmutable('2025-06-14'));

        $this->assertTrue($manager->isExpired($subscription));

        // Move time backwards
        $clock->modify('-2 days');
        $this->assertFalse($manager->isExpired($subscription));
    }
}

EventDispatcher Component

Symfony 8.0: Компоненты EventDispatcher и Event явно указаны в темах экзамена.

EventDispatcher реализует паттерн Observer. Компоненты диспатчат события, слушатели реагируют.

<?php

declare(strict_types=1);

namespace App\Event;

use Symfony\Contracts\EventDispatcher\Event;

final class OrderPlacedEvent extends Event
{
    public function __construct(
        private readonly Order $order,
    ) {
    }

    public function getOrder(): Order
    {
        return $this->order;
    }
}
<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class OrderNotificationSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderPlacedEvent::class => [
                ['sendConfirmation', 10],   // priority 10 -- first
                ['updateInventory', 0],      // priority 0 -- second
                ['trackAnalytics', -10],     // priority -10 -- last
            ],
        ];
    }

    public function sendConfirmation(OrderPlacedEvent $event): void
    {
        $order = $event->getOrder();
        // Send email confirmation
    }

    public function updateInventory(OrderPlacedEvent $event): void
    {
        // Reduce stock
    }

    public function trackAnalytics(OrderPlacedEvent $event): void
    {
        // Track conversion
    }
}

Диспатч события

<?php

declare(strict_types=1);

namespace App\Service;

use App\Event\OrderPlacedEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class OrderService
{
    public function __construct(
        private readonly EventDispatcherInterface $dispatcher,
    ) {
    }

    public function placeOrder(Cart $cart): Order
    {
        $order = Order::fromCart($cart);

        // Dispatch event -- all subscribers notified
        $this->dispatcher->dispatch(new OrderPlacedEvent($order));

        return $order;
    }
}

Event Listener через атрибут

<?php

declare(strict_types=1);

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;

#[AsEventListener(event: ExceptionEvent::class, priority: 10)]
final class ExceptionListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        // Handle exception
    }
}

Process Component

Process выполняет системные команды из PHP.

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

final class ImageOptimizer
{
    public function optimize(string $filePath): void
    {
        $process = new Process([
            'optipng',
            '-o5',
            $filePath,
        ]);

        $process->setTimeout(120);
        $process->run();

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }
    }

    public function convertVideo(string $input, string $output): void
    {
        $process = new Process([
            'ffmpeg',
            '-i', $input,
            '-codec', 'h264',
            $output,
        ]);

        $process->setTimeout(3600);

        // Run with real-time output
        $process->run(function (string $type, string $buffer): void {
            if (Process::ERR === $type) {
                // stderr output
            } else {
                // stdout output
            }
        });
    }
}

PropertyAccess Component

PropertyAccess читает и записывает значения объектов и массивов через строковые пути.

<?php

declare(strict_types=1);

use Symfony\Component\PropertyAccess\PropertyAccess;

$accessor = PropertyAccess::createPropertyAccessor();

// Array access
$data = ['user' => ['name' => 'John', 'address' => ['city' => 'Paris']]];
$city = $accessor->getValue($data, '[user][address][city]');
// Result: 'Paris'

// Object access
$name = $accessor->getValue($user, 'name');          // calls getName()
$city = $accessor->getValue($user, 'address.city');   // calls getAddress()->getCity()

// Write
$accessor->setValue($user, 'name', 'Jane');            // calls setName('Jane')
$accessor->setValue($data, '[user][name]', 'Jane');    // $data['user']['name'] = 'Jane'

// Check readable/writable
$accessor->isReadable($user, 'email');    // true if getEmail() exists
$accessor->isWritable($user, 'email');    // true if setEmail() exists

Runtime Component

Symfony 8.0: Компонент Runtime явно включён в темы экзамена.

Runtime component отвечает за инициализацию приложения. Он декоплит бутстрап приложения от его кода.

<?php

declare(strict_types=1);

// public/index.php
use App\Kernel;

require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';

// Runtime component handles:
// 1. Reading $_SERVER variables
// 2. Creating Request
// 3. Booting Kernel
// 4. Handling Request
// 5. Sending Response
// 6. Terminating Kernel

return function (array $context) {
    return new Kernel(
        $context['APP_ENV'],
        (bool) $context['APP_DEBUG'],
    );
};

Lock Component

Symfony 8.0: Компонент Lock УДАЛЁН из списка тем экзамена.

Lock предотвращает одновременное выполнение кода несколькими процессами. Хотя компонент не в экзамене Sf8, он полезен на практике.

<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Lock\LockFactory;

#[AsCommand(name: 'app:import')]
final class ImportCommand extends Command
{
    public function __construct(
        private readonly LockFactory $lockFactory,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $lock = $this->lockFactory->createLock('import-data', 3600);

        if (!$lock->acquire()) {
            $output->writeln('Import already running.');
            return Command::SUCCESS;
        }

        try {
            // Do import work
            return Command::SUCCESS;
        } finally {
            $lock->release();
        }
    }
}

Deployment Best Practices

Checklist для deployment

# 1. Install dependencies (no dev)
composer install --no-dev --optimize-autoloader

# 2. Clear and warm up cache
php bin/console cache:clear --env=prod
php bin/console cache:warmup --env=prod

# 3. Compile .env to PHP
composer dump-env prod

# 4. Run database migrations
php bin/console doctrine:migrations:migrate --no-interaction

# 5. Install assets
php bin/console assets:install public

# 6. Dump Composer autoloader
composer dump-autoload --optimize --classmap-authoritative

APP_ENV=prod

# .env.local.php (compiled env)
# Generated by: composer dump-env prod

В production:

  • APP_ENV=prod и APP_DEBUG=0
  • .env файлы НЕ используются (только системные переменные)
  • Кеш полностью прогрет
  • Autoloader оптимизирован

Web Profiler и Debug Toolbar

Symfony 8.0: WebProfiler bundle явно включён в темы экзамена.

Web Profiler -- инструмент разработки, показывающий подробную информацию о каждом запросе.

Что показывает Debug Toolbar

Панель Информация
Performance Время ответа, memory usage
Request Маршрут, контроллер, параметры
Doctrine SQL-запросы, количество, время
Twig Шаблоны, время рендеринга
Security Пользователь, firewall, токен
Logger Логи, предупреждения, ошибки
Events Вызванные события и слушатели
Forms Дерево форм, данные, ошибки
Validator Нарушения валидации
Mailer Отправленные email
Cache Cache hits/misses

Конфигурация

# config/packages/web_profiler.yaml
when@dev:
    web_profiler:
        toolbar: true
        intercept_redirects: false

    framework:
        profiler:
            only_exceptions: false

Доступ к профайлеру

# Toolbar appears at bottom of HTML pages in dev
# Full profiler at:
http://localhost:8000/_profiler/

# Specific request token:
http://localhost:8000/_profiler/{token}

# Latest request:
http://localhost:8000/_profiler/latest

# Search by URL, method, status code:
http://localhost:8000/_profiler/search

Итоги

  • Clock -- абстракция времени, MockClock для тестов
  • EventDispatcher -- Observer паттерн, #[AsEventListener]
  • Process -- запуск системных команд из PHP
  • PropertyAccess -- чтение/запись по строковому пути
  • Runtime -- бутстрап приложения (index.php)
  • Lock -- предотвращение конкурентного выполнения (не в Sf8 экзамене)
  • Deployment: --no-dev, cache:warmup, dump-env prod
  • WebProfiler -- debug toolbar и профайлер запросов