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 и профайлер запросов