Hard📖Теория6 min

Argument Resolvers

Встроенные resolvers, EntityValueResolver, custom resolver, #[MapEntity], приоритеты

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.


Проверь себя

🧪

Что делает `#[MapEntity(expr: 'repository.findOneBySlug(slug)')]`?

🧪

Какой интерфейс используется для custom argument resolver в Symfony 7+/8.0?

🧪

Что произойдёт если BackedEnum не содержит значение из URL?

🧪

Можно ли инжектить сервисы в action-методы контроллера без AbstractController?

🧪

Что произойдёт, если EntityValueResolver не найдёт сущность по id?