Argument Resolvers
Argument Value Resolvers автоматически определяют, какие значения передать в аргументы контроллера. На экзамене проверяют встроенные resolvers, порядок их выполнения и создание custom resolvers.
Как работает Argument Resolving
При вызове контроллера Symfony анализирует каждый параметр метода и пытается подобрать значение через цепочку resolvers.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Product;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ExampleController extends AbstractController
{
#[Route('/products/{id}')]
public function show(
int $id, // ← RequestAttributeValueResolver
Request $request, // ← RequestValueResolver
SessionInterface $session, // ← SessionValueResolver
?UserInterface $user, // ← SecurityTokenValueResolver
Product $product, // ← EntityValueResolver
): Response {
return $this->json(['id' => $id]);
}
}
Встроенные Argument Resolvers
Порядок выполнения (по приоритету)
| # | Resolver | Priority | Что resolve | Тип аргумента |
|---|---|---|---|---|
| 1 | RequestAttributeValueResolver |
100 | Route parameters | Совпадение по имени |
| 2 | RequestValueResolver |
50 | Объект Request | Request |
| 3 | SessionValueResolver |
50 | Объект Session | SessionInterface |
| 4 | SecurityTokenValueResolver |
40 | Текущий пользователь | UserInterface |
| 5 | ServiceValueResolver |
-50 | Сервисы из контейнера | Любой сервис |
| 6 | DefaultValueResolver |
-100 | Значение по умолчанию | Параметр с default |
| 7 | BackedEnumValueResolver |
100 | Enum из route param | BackedEnum |
| 8 | DateTimeValueResolver |
100 | DateTime из string | \DateTimeInterface |
| 9 | UidValueResolver |
100 | UUID/ULID | AbstractUid |
RequestAttributeValueResolver
Извлекает значения из $request->attributes (route parameters).
<?php
declare(strict_types=1);
// Route: /products/{id}/{slug}
#[Route('/products/{id}/{slug}')]
public function show(int $id, string $slug): Response
{
// $id ← from route parameter {id}
// $slug ← from route parameter {slug}
// Matched by parameter NAME
return $this->json(['id' => $id, 'slug' => $slug]);
}
RequestValueResolver
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
// Injects the current Request object
public function action(Request $request): Response
{
$method = $request->getMethod();
return $this->json(['method' => $method]);
}
BackedEnumValueResolver
<?php
declare(strict_types=1);
namespace App\Enum;
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
}
<?php
declare(strict_types=1);
use App\Enum\Status;
// Route parameter automatically resolved to Enum
#[Route('/items/{status}')]
public function byStatus(Status $status): Response
{
// /items/active → Status::Active
// /items/invalid → 404 (auto-generated requirement)
return $this->json(['status' => $status->value]);
}
Подвох экзамена:
BackedEnumValueResolverавтоматически генерирует requirements для route parameter на основе enum cases. ДляStatusэто будетactive|inactive|pending. Невалидное значение автоматически возвращает 404, а не exception.
DateTimeValueResolver
<?php
declare(strict_types=1);
#[Route('/events/{date}')]
public function byDate(\DateTimeInterface $date): Response
{
// /events/2026-01-15 → DateTimeImmutable('2026-01-15')
return $this->json(['date' => $date->format('Y-m-d')]);
}
// With custom format
#[Route('/events/{date}')]
public function byDateFormatted(
#[\Symfony\Component\HttpKernel\Attribute\MapDateTime(format: 'd-m-Y')]
\DateTimeInterface $date,
): Response {
// /events/15-01-2026 → DateTimeImmutable('2026-01-15')
return $this->json(['date' => $date->format('Y-m-d')]);
}
UidValueResolver
<?php
declare(strict_types=1);
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\Ulid;
#[Route('/products/{id}')]
public function show(Uuid $id): Response
{
// /products/550e8400-e29b-41d4-a716-446655440000 → Uuid object
return $this->json(['id' => $id->toRfc4122()]);
}
#[Route('/orders/{id}')]
public function order(Ulid $id): Response
{
// /orders/01ARZ3NDEKTSV4RRFFQ69G5FAV → Ulid object
return $this->json(['id' => (string) $id]);
}
ServiceValueResolver
<?php
declare(strict_types=1);
use App\Service\ProductService;
class ProductController extends AbstractController
{
// Service auto-injected into action method
// Works because AbstractController / #[AsController] enables this
#[Route('/products')]
public function list(ProductService $productService): Response
{
$products = $productService->findAll();
return $this->json($products);
}
}
Подвох экзамена:
ServiceValueResolverинжектит сервисы в action-методы только если контроллер помеченcontroller.service_argumentsтегом.AbstractControllerи#[AsController]добавляют этот тег автоматически. Обычный класс без этих маркеров НЕ получит сервисы в action-методы.
EntityValueResolver (#[MapEntity])
EntityValueResolver (из DoctrineBundle) автоматически загружает Doctrine-сущности.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Product;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
class ProductController extends AbstractController
{
// Auto-resolve by id (default behavior)
#[Route('/products/{id}')]
public function show(Product $product): Response
{
// SELECT * FROM product WHERE id = :id
// If not found → 404 automatically
return $this->json($product);
}
// Resolve by custom field
#[Route('/products/{slug}')]
public function showBySlug(
#[MapEntity(mapping: ['slug' => 'slug'])]
Product $product,
): Response {
// SELECT * FROM product WHERE slug = :slug
return $this->json($product);
}
// Multiple fields mapping
#[Route('/products/{category_slug}/{product_slug}')]
public function showInCategory(
#[MapEntity(mapping: ['category_slug' => 'slug'])]
Category $category,
#[MapEntity(mapping: ['product_slug' => 'slug'])]
Product $product,
): Response {
return $this->json(['category' => $category, 'product' => $product]);
}
// Custom parameter name for id
#[Route('/products/{product_id}/reviews/{review_id}')]
public function review(
#[MapEntity(id: 'product_id')] Product $product,
#[MapEntity(id: 'review_id')] Review $review,
): Response {
return $this->json([]);
}
// Using repository expression
#[Route('/products/{slug}')]
public function findByExpression(
#[MapEntity(expr: 'repository.findOneBySlug(slug)')]
Product $product,
): Response {
return $this->json($product);
}
// Disable auto-resolution
#[Route('/products/{id}')]
public function rawId(
#[MapEntity(disabled: true)]
int $id,
): Response {
// Just receives raw string/int value, no DB query
return $this->json(['id' => $id]);
}
// Optional entity (nullable)
#[Route('/products/{id}')]
public function optional(
#[MapEntity] ?Product $product,
): Response {
// If entity not found → $product = null (no 404)
if ($product === null) {
throw $this->createNotFoundException();
}
return $this->json($product);
}
}
Все параметры #[MapEntity]
| Параметр | Тип | Описание |
|---|---|---|
id |
string | Имя route-параметра для id |
mapping |
array | Маппинг route param → entity field |
expr |
string | Expression Language для поиска |
class |
string | Класс entity (если не определяется по type-hint) |
objectManager |
string | Имя object manager |
entityManager |
string | Имя entity manager |
evictCache |
bool | Не использовать result cache |
disabled |
bool | Отключить auto-resolution |
Подвох экзамена: Если параметр type-hint
Productи route parameter{id}, Symfony автоматически ищетProductпоidБЕЗ#[MapEntity]. Атрибут нужен только для нестандартных случаев: поиск по slug, несколько entities, custom expression.
Создание custom Argument Resolver
<?php
declare(strict_types=1);
namespace App\ArgumentResolver;
use App\DTO\CurrentUserContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
// ValueResolverInterface — the modern interface (Symfony 6.2+)
final class CurrentUserContextResolver implements ValueResolverInterface
{
public function __construct(
private readonly UserContextService $userContextService,
) {
}
/**
* @return iterable<CurrentUserContext>
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
// Only resolve for our specific type
if ($argument->getType() !== CurrentUserContext::class) {
return [];
}
yield $this->userContextService->getCurrentContext();
}
}
Регистрация custom resolver
# config/services.yaml
services:
App\ArgumentResolver\CurrentUserContextResolver:
tags:
- { name: controller.argument_value_resolver, priority: 150 }
<?php
declare(strict_types=1);
// Or using PHP attribute for auto-configuration
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('controller.argument_value_resolver', ['priority' => 150])]
final class CurrentUserContextResolver implements ValueResolverInterface
{
// ...
}
Использование в контроллере
<?php
declare(strict_types=1);
class DashboardController extends AbstractController
{
#[Route('/dashboard')]
public function index(CurrentUserContext $context): Response
{
// Custom resolver provides CurrentUserContext automatically
return $this->render('dashboard/index.html.twig', [
'context' => $context,
]);
}
}
Подвох экзамена: В Symfony 6.2+ интерфейс
ValueResolverInterfaceзаменилArgumentValueResolverInterface. Старый интерфейс deprecated в 6.2 и удалён в 7.0. Новый интерфейс имеет один методresolve(), который возвращаетiterable(пустой = не обрабатывает).
#[ValueResolver] — указание конкретного resolver
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
class ProductController extends AbstractController
{
// Force specific resolver for this argument
#[Route('/products/{id}')]
public function show(
#[ValueResolver(ProductValueResolver::class)]
Product $product,
): Response {
return $this->json($product);
}
// Disable all resolvers for an argument
#[Route('/items/{id}')]
public function item(
#[ValueResolver(disabled: true)]
string $id,
): Response {
// No resolver will process this — raw value from route
return $this->json(['id' => $id]);
}
}
ArgumentMetadata
<?php
declare(strict_types=1);
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
// ArgumentMetadata provides info about the controller parameter
// Available in resolve() method:
$argument->getName(); // Parameter name: 'product'
$argument->getType(); // Type hint: 'App\Entity\Product'
$argument->isNullable(); // Has ? prefix: true
$argument->hasDefaultValue(); // Has default value: false
$argument->getDefaultValue(); // Default value: null
$argument->isVariadic(); // Is ...$args: false
$argument->getAttributes(); // PHP attributes on parameter
$argument->getAttributesOfType(MapEntity::class); // Specific attributes
Порядок разрешения аргументов
Controller method: show(int $id, Product $product, Request $request)
1. BackedEnumValueResolver (100) — нет, int не enum → skip
2. DateTimeValueResolver (100) — нет, int не DateTime → skip
3. UidValueResolver (100) — нет, int не Uuid → skip
4. RequestAttributeValueResolver (100) — $id есть в route params → RESOLVED ✓
5. Переходим к $product:
- EntityValueResolver — Product есть entity, {id} route param → RESOLVED ✓
6. Переходим к $request:
- RequestValueResolver (50) — type-hint Request → RESOLVED ✓
Подвох экзамена: Resolvers с более высоким priority выполняются первыми. Если resolver возвращает значение — остальные resolvers для этого аргумента пропускаются. Если ни один resolver не вернул значение и нет default — бросается
RuntimeException.