Compiler Passes
Compiler Pass -- механизм для модификации контейнера на этапе компиляции. Позволяет программно изменять определения сервисов, добавлять теги, модифицировать аргументы.
Жизненный цикл компиляции
1. Load config (YAML, PHP attributes)
2. Run Compiler Passes:
a. BeforeOptimizationPass
b. OptimizationPass (resolves parameters, aliases)
c. BeforeRemovingPass
d. RemovingPass (removes private unused services)
e. AfterRemovingPass
3. Dump compiled container
Создание Compiler Pass
<?php
declare(strict_types=1);
namespace App\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
final class MessageHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
// Check if the main service exists
if (!$container->has(MessageBus::class)) {
return;
}
$definition = $container->findDefinition(MessageBus::class);
// Find all services tagged with 'app.message_handler'
$taggedServices = $container->findTaggedServiceIds('app.message_handler');
foreach ($taggedServices as $id => $tags) {
// Add each handler as a method call
$definition->addMethodCall('addHandler', [new Reference($id)]);
}
}
}
Регистрация Compiler Pass
<?php
declare(strict_types=1);
namespace App;
use App\DependencyInjection\Compiler\MessageHandlerPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
final class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void
{
// Default priority type: BeforeOptimizationPass
$container->addCompilerPass(new MessageHandlerPass());
// Or with explicit pass type and priority
$container->addCompilerPass(
new MessageHandlerPass(),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
0 // priority
);
}
}
Типы Compiler Passes
| Тип | Константа | Когда запускается |
|---|---|---|
| Before optimization | TYPE_BEFORE_OPTIMIZATION |
До оптимизации (default) |
| Optimization | TYPE_OPTIMIZE |
Фаза оптимизации |
| Before removing | TYPE_BEFORE_REMOVING |
До удаления сервисов |
| Removing | TYPE_REMOVE |
Фаза удаления |
| After removing | TYPE_AFTER_REMOVING |
После удаления |
Пример: сбор всех стратегий
<?php
declare(strict_types=1);
namespace App\DependencyInjection\Compiler;
use App\Pricing\PricingEngine;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
final class PricingStrategyPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(PricingEngine::class)) {
return;
}
$engineDefinition = $container->findDefinition(PricingEngine::class);
$strategies = $container->findTaggedServiceIds('app.pricing_strategy');
// Sort by priority
$sortedStrategies = [];
foreach ($strategies as $id => $tags) {
foreach ($tags as $attributes) {
$priority = $attributes['priority'] ?? 0;
$sortedStrategies[$priority][] = $id;
}
}
krsort($sortedStrategies);
foreach ($sortedStrategies as $ids) {
foreach ($ids as $id) {
$engineDefinition->addMethodCall(
'addStrategy',
[new Reference($id)]
);
}
}
}
}
Фабрики (Factories)
Фабрика -- альтернативный способ создания сервисов, когда конструктор недоступен или недостаточен.
Статическая фабрика
<?php
declare(strict_types=1);
namespace App\Service;
final class DatabaseConnection
{
private function __construct(
private readonly \PDO $pdo,
) {
}
public static function create(string $dsn, string $user, string $pass): self
{
return new self(new \PDO($dsn, $user, $pass, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
]));
}
}
services:
App\Service\DatabaseConnection:
factory: ['App\Service\DatabaseConnection', 'create']
arguments:
- '%env(DATABASE_DSN)%'
- '%env(DATABASE_USER)%'
- '%env(DATABASE_PASS)%'
Фабрика через отдельный класс
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Service\CacheAdapter;
final class CacheAdapterFactory
{
public function __construct(
private readonly string $cacheDriver,
private readonly string $cacheHost,
) {
}
public function create(): CacheAdapter
{
return match ($this->cacheDriver) {
'redis' => new RedisCacheAdapter($this->cacheHost),
'memcached' => new MemcachedCacheAdapter($this->cacheHost),
default => new FileCacheAdapter('/tmp/cache'),
};
}
}
services:
App\Factory\CacheAdapterFactory:
arguments:
$cacheDriver: '%env(CACHE_DRIVER)%'
$cacheHost: '%env(CACHE_HOST)%'
App\Service\CacheAdapter:
factory: ['@App\Factory\CacheAdapterFactory', 'create']
Invokable фабрика
<?php
declare(strict_types=1);
namespace App\Factory;
final class LoggerFactory
{
public function __invoke(string $channel): LoggerInterface
{
return new Logger($channel, [
new StreamHandler('php://stderr'),
]);
}
}
services:
App\Factory\LoggerFactory: ~
custom_logger:
factory: '@App\Factory\LoggerFactory'
arguments: ['app']
Синтетические сервисы
Синтетический сервис -- сервис, определение которого известно контейнеру, но экземпляр устанавливается извне (в runtime).
services:
# Synthetic service -- container doesn't create it
kernel:
synthetic: true
request:
synthetic: true
<?php
declare(strict_types=1);
// In kernel boot or front controller
$container = $kernel->getContainer();
$container->set('some_external_service', $externalService);
Синтетические сервисы используются редко. Ядро Symfony (Kernel) -- пример синтетического сервиса, так как оно создаётся до контейнера.
Lazy-сервисы
Lazy-сервис -- сервис, который создаётся только при первом обращении к нему. Полезно для тяжёлых сервисов, которые не всегда нужны.
Через YAML
services:
App\Service\HeavyReportGenerator:
lazy: true
Через атрибут
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(lazy: true)]
final class HeavyReportGenerator
{
public function __construct()
{
// Expensive initialization: DB connections, file loading, etc.
sleep(2); // Simulating heavy init
}
public function generate(array $data): string
{
return 'Generated report';
}
}
Lazy через #[Autowire]
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Service\HeavyReportGenerator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class ReportController
{
public function __construct(
// This service will be lazy-loaded
#[Autowire(lazy: true)]
private readonly HeavyReportGenerator $reportGenerator,
) {
}
public function index(): Response
{
// HeavyReportGenerator is NOT instantiated until this line
$report = $this->reportGenerator->generate($data);
return new Response($report);
}
}
Как работает lazy
Symfony создаёт ghost-proxy объект. Этот прокси реализует тот же интерфейс, но конструктор реального сервиса вызывается только при первом обращении к методу.
Injection -> Ghost Proxy (lightweight) -> First method call -> Real instantiation
Абстрактные определения
Абстрактные сервисы служат шаблоном для других определений.
services:
# Abstract definition -- template
app.abstract_repository:
abstract: true
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$logger: '@logger'
# Concrete services inherit from abstract
App\Repository\UserRepository:
parent: app.abstract_repository
arguments:
$entityClass: App\Entity\User
App\Repository\OrderRepository:
parent: app.abstract_repository
arguments:
$entityClass: App\Entity\Order
Deprecation сервисов
services:
App\Service\OldMailer:
deprecated:
package: 'app'
version: '2.0'
message: 'The "%service_id%" service is deprecated, use App\Service\NewMailer instead.'
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(
deprecated: 'The "%service_id%" is deprecated, use NewMailer instead.',
)]
final class OldMailer
{
// ...
}
Практический пример: Plugin System
<?php
declare(strict_types=1);
namespace App\Plugin;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
// Interface with auto-tagging
#[AutoconfigureTag('app.plugin')]
interface PluginInterface
{
public function getName(): string;
public function execute(array $context): mixed;
}
<?php
declare(strict_types=1);
namespace App\Plugin;
final class AnalyticsPlugin implements PluginInterface
{
public function getName(): string
{
return 'analytics';
}
public function execute(array $context): mixed
{
return ['pageviews' => 1000];
}
}
<?php
declare(strict_types=1);
namespace App\DependencyInjection\Compiler;
use App\Plugin\PluginManager;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
final class PluginPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(PluginManager::class)) {
return;
}
$manager = $container->findDefinition(PluginManager::class);
$plugins = $container->findTaggedServiceIds('app.plugin');
foreach ($plugins as $id => $tags) {
$manager->addMethodCall('register', [new Reference($id)]);
}
}
}
<?php
declare(strict_types=1);
namespace App\Plugin;
final class PluginManager
{
/** @var array<string, PluginInterface> */
private array $plugins = [];
public function register(PluginInterface $plugin): void
{
$this->plugins[$plugin->getName()] = $plugin;
}
public function execute(string $name, array $context = []): mixed
{
if (!isset($this->plugins[$name])) {
throw new \RuntimeException("Plugin '$name' not found");
}
return $this->plugins[$name]->execute($context);
}
/** @return list<string> */
public function getRegistered(): array
{
return array_keys($this->plugins);
}
}
Итоги
- Compiler Pass модифицирует контейнер при компиляции
findTaggedServiceIds()-- основной метод для сбора тегированных сервисов- Фабрики полезны, когда конструктор не подходит для создания объекта
- Синтетические сервисы создаются снаружи контейнера
- Lazy-сервисы создаются при первом использовании через ghost-proxy
- Абстрактные определения -- шаблоны для наследования конфигурации