Расширение разрещения аргумента действия

Дата обновления перевода 2024-07-17

Расширение разрещения аргумента действия

В руководстве о контроллерах, вы узнали, что вы можете получить объект Request через аргумент в вашем контроллере. Этот аргумент должен иметь подсказку в виде класса Request, чтобы быть распознанным. Это делается через ArgumentResolver. Создавая и регистрируя пользовательские разрешители значений, вы можете расширить эту функциональность.

Встроенные разрешители значений

Symfony поставляется со следующими разрешителями значений в компоненте HttpKernel:

BackedEnumValueResolver

Пытается разрешить случай исчисляемого бэк-энда из параметра пути маршрута, который совпадает с именем аругмента. Ведёт к ответу 404 "Не найдено", если значение не является валидным опорным значением для типа исчисления.

Например, если ваше исчисление бэк-энда:

1
2
3
4
5
6
7
8
9
namespace App\Model;

enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

А ваш контроллер содержит следующее:

1
2
3
4
5
6
7
8
9
10
class CardController
{
    #[Route('/cards/{suit}')]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

При запросе URL /cards/H, переменная $suit будет хранить случай Suit::Hearts.

Более того, вы можете ограничить разрешённые значения параметра маршрута до одного (или более) с помощью EnumRequirement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Routing\Requirement\EnumRequirement;

// ...

class CardController
{
    #[Route('/cards/{suit}', requirements: [
        // это позволяет все значения, определённые в Enum
        'suit' => new EnumRequirement(Suit::class),
        // это ограничивает возможные значения до значений Enum, перечисленных здесь
        'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]),
    ])]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

Пример выше позволяет запрос только URL /cards/D и /cards/S, и приводит к ответу 404 "Не найдено" в двух других случаях.

RequestPayloadValueResolver сопоставляет полезную нагрузку запроса или строку запроса с объектом с подсказкой типа.

Поскольку это целевой разрешитель значений , вам придется использовать либо MapRequestPayload либо атрибут MapQueryString . для использования этого разрешителя.

RequestAttributeValueResolver
Пробует найти атрибут запроса, который совпадает с именем аргумента.
DateTimeValueResolver

Пробует найти атрибут запроса, который совпадает с именем аргумента и внедряет объект DateTimeInterface, если есть подсказка класса, расширяющего DateTimeInterface.

По умолчанию, любой ввод, который можно разобрать как строку даты путём PHP, принимается. Вы можете ограничить то, как может быть отформатирован ввод, с помощью атрибута MapDateTime.

Tip

Объект DateTimeInterface создается с помощью компонента Clock. Это дает вам полный контроль над значениями даты и времени, которые контроллер получает при тестировании вашего приложения и использовании реализации MockClock.

RequestValueResolver
Внедряет текущий Request, если есть подсказка Request или класса, расширяющего Request.
ServiceValueResolver
Внедряет сервис, если есть подсказка в виде валидного класса сервиса или интерфейса. Это работает как автомонтирование.
SessionValueResolver
Внедряет сконфигурированный класс сессии, реализующий SessionInterface, если есть подсказка SessionInterface или класса, реализующего SessionInterface.
DefaultValueResolver
Установит значение по умолчанию для аргумента, если он присутствует и аргумент необязательный.
UidValueResolver

Пробует прреобразовать любые значения UID из параметра пути маршрута в объекты UID. Приводит к ответу 404 "Не найдено", если значение не является валидным UID.

Например, следующее преобразует параметр токена в объект UuidV4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\UuidV4;

class DefaultController
{
    #[Route('/share/{token}')]
    public function share(UuidV4 $token): Response
    {
        // ...
    }
}
VariadicValueResolver
Верифицирует, являются ли данные запроса массивом, и добавит их все к списку аргументов. Когда вызывается действие, последний (вариативный) аргумент будет содержать все значения этого массива.

Кроме того, некоторые компоненты и официальные пакеты предоставляют другие разрешители значений:

UserValueResolver

Внедряет объект, который представляет текущего пользователя в системе, если есть подсказка UserInterface. Вы можете также добавить подсказку собственного класса User, но вы должны затем добавить атрибут #[CurrentUser] к аргументу. Значение по умолчанию может быть установлено как null в случае, если к контроллеру можно получить доступ анонимным пользователям. Требует установки SecurityBundle.

Если аргумент не может быть null и нет пользователя в системе, или пользователь в системе имеет класс пользователя, не совпадающий с классом подсказки, разрешителем вызыввается AccessDeniedException, чтобы предотвратить доступ к контроллеру.

SecurityTokenValueResolver

Внедряет объект, представляющий текущий токен входа в систему, если тип подсказан с помощью TokenInterface или класса, расширяющего его.

Если аргумент не является нулевым и не существует токена входа в систему, разрешителем вызывается HttpException с кодом состояния 401, чтобы предотвратить доступ к контроллеру.

EntityValueResolver

Автоматически запрашивайте сущность и передавайте ее в качестве аргумента вашему контроллеру.

Например, следующее запросит сущность Product, которая имеет {id} в качестве основного ключа:

// src/Controller/DefaultController.php namespace AppController;

use SymfonyComponentHttpFoundationResponse; use SymfonyComponentRoutingAttributeRoute;

class DefaultController { #[Route('/product/{id}')] public function share(Product $product): Response { // ... } }

Чтобы узнать больше об использовании EntityValueResolver, обратитесь к специальному разделу Автоматическое получение объектов .

Разрешитель объектов PSR-7:
Внедряет объект Symfony HttpFoundation Request, созданный из объекта PSR-7 типа Psr\\Http\\Message\\ServerRequestInterface, Psr\\Http\\Message\\RequestInterface или Psr\\Http\\Message\\MessageInterface. Требует установки компонента Мост PSR-7.

Управление разрешителями значений

Для каждого аргумента будет вызываться каждый разрешитель с тегом controller.argument_value_resolver до тех пор, пока один из них не предоставит значение. Порядок их вызова зависит от от их приоритета. Например, SessionValueResolver будет вызван раньше, чем DefaultValueResolver, поскольку его приоритет выше. Это позволяет написать, например, SessionInterface $session = null, чтобы получить сессию, если она есть, или null, если её нет.

В этом конкретном случае вам не нужно запускать никакой разрешитель перед SessionValueResolver, поэтому их пропуск не только повысит производительность, но и не позволит одному из них предоставить значение раньше, чем это сделает SessionValueResolver.

Атрибут ValueResolver позволяет сделать это, "нацеливаясь" на нужный разрешитель:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/SessionController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver;
use Symfony\Component\Routing\Attribute\Route;

class SessionController
{
    #[Route('/')]
    public function __invoke(
        #[ValueResolver(SessionValueResolver::class)]
        SessionInterface $session = null
    ): Response
    {
        // ...
    }
}

В приведенном выше примере SessionValueResolver будет вызван первым, поскольку поскольку он является целевым. Следующим будет вызван DefaultValueResolver, если значение не было предоставлено; поэтому в качестве значения по умолчанию для $session вы можете назначить null.

Вы можете нацелить разрешитель, передав его имя в качестве первого аргумента ValueResolver. Для удобства имена встроенных разрешителей - это их FQCN.

Целевой разрешитель можно также отключить, передав аргументу ValueResolver $disabled значение true; именно так MapEntity позволяет отключить EntityValueResolver для конкретного контроллера . Да, MapEntity расширяет ValueResolver!

Добавление пользовательского разрешителя значений

В следующем примере вы создадите разрешитель значений для внедрения объекта значения ID, если аргумент контроллера имеет тип, реализующий IdentifierInterface
(например, BookingId):

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;

class BookingController
{
    public function index(BookingId $id): Response
    {
        // ... сделать что-то с $id
    }
}

Добавление нового разрешителя значений требует создания класса, который реализует ValueResolverInterface, и определения сервиса для него.

Этот интерфейс содержит метод resolve(), который вызывается для каждого аргумента контроллера. Он получает текущий объект Request и экземпляр ArgumentMetadata, который содержит всю информацыию из подписи метода.

Метод resolve() должен вернуть либо пустой массив (если он не может разрешить этот аргумент) или массив с разрешённым(и) значениям(и). Обычно аргументы разрешаются как одно значение, но вариативные аргументы требуют разрешения нескольких значений. Поэтому вы должна всегда возвращать массив, даже для одиночных значений:

// src/ValueResolver/IdentifierValueResolver.php namespace AppValueResolver;

use AppIdentifierInterface; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpKernelControllerValueResolverInterface; use SymfonyComponentHttpKernelControllerMetadataArgumentMetadata;

class BookingIdValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { // получить тип аргумента (например, BookingId) $argumentType = $argument->getType(); if ( !$argumentType || !is_subclass_of($argumentType, IdentifierInterface::class, true) ) { return []; }

// получить значение из запроса, основываясь на имени аргумента

$value = $request->attributes->get($argument->getName()); if (!is_string($value)) { return []; }

// создать и вернуть объект значения return [$argumentType::fromString($value)];

}

}

Этот метод сначала проверяет, может ли он разрешить значение:

  • Аргумент должен иметь подсказку класса, реализующего пользовательский IdentifierInterface;
  • Имя аргумента (например, $id) должно совпадать с именем атрибута запроса (например, используя заполнитель маршрута /booking/{id}).

Когда эти требования выполнены, метод создаёт новый экземпляр пользовательского объекта значения и возвращает его как значение этого аргумента.

Это всё! Теперь всё, что вам нужно сделать, - это добавить конфигурацию для сервис-контейнера. Это можно сделать добавив к вашему разрешителю значений один из следующих тегов.

controller.argument_value_resolver

Этот тег автоматически добавляется к каждому сервису, реализующему ValueResolverInterface, но вы можете установить его самостоятельно, чтобы изменить его атрибуты priority или name.

1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    _defaults:
        # ... убедитесь в том, что включено автомонтирование
        autowire: true
    # ...

    App\ValueResolver\BookingIdValueResolver:
        tags:
            - controller.argument_value_resolver:
                name: booking_id
                priority: 150

Хотя добавление приоритета является необязательным, рекомендуется его добавить, чтобы гарантировать, что ожидаемое значение будет внедрено. Встроенный RequestAttributeValueResolver, который также извлекает атрибуты Request, должен иметь приоритет 100 или более. В других случаях, установите приоритет ниже 100, чтобы убедиться, что разрешитель аргументов не запускается, когда присутствует атрибут Request.

Чтобы гарантировать, что ваши разрешители добавляются в правильном месте, вы можете выполнить следующую команду, чтобы увидеть, какие разрешители аргументов присутствуют, и в каком порядке они запукаются:

1
$ php bin/console debug:container debug.argument_resolver.inner --show-arguments

Вы также можете сконфигурировать имя, переданное атрибуту ValueResolver, чтобы указать ваш разрешитель. Иначе он по умолчанию будет id сервиса.

controller.targeted_value_resolver

Установите этот тег, если вы хотите, чтобы ваш разрешитель вызывался только в том случае, если на него указывает атрибут ValueResolver. Как и в случае с controller.argument_value_resolver, вы можете настроить имя, под которым будет вызываться ваш разрешитель.

В качестве альтернативы вы можете добавить атрибут AsTargetedValueResolver к своему
разрешителю и передавать в качестве первого аргумента ваше пользовательское имя:

1
2
3
4
5
6
7
8
9
10
11
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;

#[AsTargetedValueResolver('booking_id')]
class BookingIdValueResolver implements ValueResolverInterface
{
    // ...
}

Затем вы можете передать это имя как первый аргумент ValueResolver, чтобы указать ваш разрешитель:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;

class BookingController
{
    public function index(#[ValueResolver('booking_id')] BookingId $id): Response
    {
        // ... сделать что-то с $id
    }
}