Подвохи Dependency Injection
Подвох 1: Private services и доступ из контроллера
По умолчанию все сервисы в Symfony -- private. Это значит, что вы не можете получить их через $container->get(). Однако контроллеры -- исключение.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
final class DemoController extends AbstractController
{
public function index(): Response
{
// AbstractController имеет доступ к service locator
// Но ТОЛЬКО к определённым сервисам!
// Это НЕ полный контейнер, а ограниченный ServiceLocator
// Работает -- контроллер имеет доступ к router, twig и т.д.
$router = $this->container->get('router');
// НЕ работает -- App\Service\MyService нет в service locator контроллера!
// $this->container->get(MyService::class); // ServiceNotFoundException!
return new Response('ok');
}
}
На экзамене: Контроллер наследующий AbstractController имеет доступ НЕ ко всему контейнеру, а к ограниченному ServiceLocator. Единственный правильный способ получить произвольный сервис -- constructor injection или method injection через #[Autowire].
Подвох 2: Autowiring и интерфейсы
Autowiring работает по type-hint. Но если один интерфейс реализован несколькими сервисами -- autowiring ломается.
<?php
declare(strict_types=1);
namespace App\Service;
interface NotificationSenderInterface
{
public function send(string $message): void;
}
final readonly class EmailSender implements NotificationSenderInterface
{
public function send(string $message): void { /* ... */ }
}
final readonly class SmsSender implements NotificationSenderInterface
{
public function send(string $message): void { /* ... */ }
}
// ОШИБКА: Symfony не знает какой из двух сервисов подставить!
// Cannot autowire service: argument "$sender" of method "__construct()"
// references interface "NotificationSenderInterface" but no such
// service exists. You should maybe alias this interface to one
// of these existing services: "EmailSender", "SmsSender".
Решение -- alias или #[AutowireLocator]:
# services.yaml
services:
App\Service\NotificationSenderInterface: '@App\Service\EmailSender'
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Psr\Container\ContainerInterface;
final readonly class NotificationService
{
public function __construct(
#[AutowireLocator([
'email' => EmailSender::class,
'sms' => SmsSender::class,
])]
private ContainerInterface $senders,
) {
}
public function notify(string $channel, string $message): void
{
$this->senders->get($channel)->send($message);
}
}
Подвох 3: Lazy Services -- когда и как
Lazy services создаются только при первом вызове метода. Но есть подвох -- конструктор НЕ вызывается при инъекции!
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\Lazy;
#[Lazy]
final class HeavyService
{
public function __construct()
{
// Этот код НЕ выполнится при инъекции сервиса!
// Он выполнится только при первом вызове ЛЮБОГО метода.
echo "Heavy initialization..."; // вызовется лениво
}
public function process(): string
{
return 'done';
}
}
final readonly class ConsumerService
{
public function __construct(
private HeavyService $heavy, // Proxy объект, НЕ реальный HeavyService
) {
// $this->heavy -- это ghost proxy, конструктор HeavyService ещё НЕ вызван!
}
public function doWork(): string
{
// Вот СЕЙЧАС вызовется конструктор HeavyService
return $this->heavy->process();
}
}
На экзамене: Lazy proxy использует ghost-объекты (symfony/var-exporter). Конструктор lazy-сервиса вызывается при первом обращении к любому методу или свойству, а НЕ при инъекции.
Подвох 4: Компиляция контейнера и кеширование
Контейнер компилируется в PHP-класс и кеширует. Но есть нюансы.
<?php
declare(strict_types=1);
// Что кешируется при компиляции контейнера:
// 1. Дерево сервисов (какой сервис от чего зависит)
// 2. Параметры контейнера (parameters в services.yaml)
// 3. Теги сервисов (уже обработанные compiler passes)
// 4. Алиасы и привязки
// Что НЕ кешируется:
// 1. Содержимое .env файлов (читается каждый раз)
// 2. Runtime параметры
// 3. Синтетические сервисы (устанавливаются в runtime)
# services.yaml
parameters:
# Этот параметр скомпилируется в контейнер
app.static_param: 'cached_value'
# Этот параметр читается из ENV при каждом запросе
app.dynamic_param: '%env(DATABASE_URL)%'
На экзамене:
%env(...)%обрабатывается через EnvVarProcessor в runtime, а НЕ при компиляции. Если изменить.env-- значение обновится без очистки кеша. Но обычные%parameter%-- фиксируются при компиляции.
Подвох 5: Synthetic Services
Синтетические сервисы не создаются контейнером -- они устанавливаются извне.
# services.yaml
services:
app.synthetic_service:
synthetic: true
<?php
declare(strict_types=1);
// Синтетический сервис устанавливается вручную:
$container->set('app.synthetic_service', $someObject);
// Пример из Symfony: 'kernel' -- синтетический сервис!
// Он создаётся до контейнера и устанавливается в него.
На экзамене: Synthetic services нельзя автоматически создать через DI. Они используются, когда объект создаётся вне контейнера (например, сам Kernel).
Подвох 6: Autoconfigure vs Ручные теги
Autoconfigure автоматически добавляет теги к сервисам, реализующим определённые интерфейсы. Но ручной тег ПЕРЕЗАПИСЫВАЕТ autoconfigure!
services:
_defaults:
autoconfigure: true
# Autoconfigure добавит тег kernel.event_listener автоматически
# если класс использует #[AsEventListener]
App\EventListener\MyListener:
tags:
# Ручной тег ЗАМЕНЯЕТ автоматический!
- { name: kernel.event_listener, event: kernel.request, priority: 100 }
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
// Если в services.yaml есть ручной тег -- этот атрибут ИГНОРИРУЕТСЯ!
#[AsEventListener(event: RequestEvent::class, priority: 10)]
final readonly class MyListener
{
public function __invoke(RequestEvent $event): void
{
// ...
}
}
На экзамене: Если сервис имеет вручную прописанный тег того же типа, autoconfigure НЕ добавит свой тег. Ручная конфигурация имеет приоритет.
Подвох 7: Abstract Services и наследование определений
Abstract services не могут быть инстанцированы, но служат шаблоном для других.
services:
app.abstract_mailer:
abstract: true
arguments:
$transport: 'smtp'
$from: '[email protected]'
App\Service\UserMailer:
parent: app.abstract_mailer
# Наследует $transport и $from от parent
# Но НЕ наследует autowire и autoconfigure!
autowire: true
arguments:
$userRepo: '@App\Repository\UserRepository'
На экзамене: Сервисы с
parentНЕ наследуютautowire,autoconfigure,publicот_defaults. Они наследуют только от своего parent-сервиса. Нужно задавать явно.
Подвох 8: #[Autowire] для скалярных параметров
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final readonly class PaymentGateway
{
public function __construct(
// Инъекция параметра контейнера
#[Autowire('%env(STRIPE_API_KEY)%')]
private string $apiKey,
// Инъекция выражения
#[Autowire(expression: 'service("router").generate("homepage")')]
private string $homepageUrl,
// Инъекция конкретного сервиса (когда несколько реализаций)
#[Autowire(service: 'app.mailer.transactional')]
private MailerInterface $mailer,
) {
}
}
На экзамене:
#[Autowire]может инжектировать параметры (%param%), env-переменные (%env(...)%), выражения (expression:) и конкретные сервисы (service:). Не путайте синтаксис.