symfony4 Резолвер аргументов (Argument Resolver)

Резолвер в общем случае используется для обработки агрументов контроллера непосредственно ПЕРЕД запуском контроллера.

Принцип действия

  • Мы передаём в контроллер в качестве аргумента некий класс/интерфейс.
  • Скрипт, натыкаясь на неизвестный аргумент, лезет в свои резолверы проверяет, нет ли резолвера для данного класса (см. метод supports()).
  • И если находит, резолвер примет из реквеста параметры, сделает с ними нечто (для чего и нужен резолвер), и передаст нужные данные в контроллер.

Где использовать

  1. Я с помощью резолвера определяю, какой сервис использовать для работы в контроллере. Логика работы обоих сервисов и названия методов одинаковые, поэтому передаём в контроллер интерфейс, а выбирает сервис - резолвер.
  2. Также резолвер можно использовать просто для валидации параметров или проверки авторизационного токена.

PHP

Создаём класс резольвера ServiceValueResolver:

// src/ArgumentResolver/ServiceValueResolver.php
namespace App\ArgumentResolver;

use App\Service\FirstService;
use App\Service\SecondService;
use App\Service\ServiceInterface;
use Generator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

class ServiceValueResolver implements ArgumentValueResolverInterface
{
// В качестве свойств класса подгружаем сервисы из сервис-контейнера
    /**
     * @var FirstService
     */
    private $firstService;
    
    /**
     * @var SecondService
     */
    private $secondService;
    
    /**
     * @param FirstService $firstService
     * @param SecondService $secondService
     */
    public function __construct(
            FirstService $firstService, 
            SecondService $secondService
    ) {
        $this->firstService = $firstService;
        $this->secondService = $secondService;
    }

    // Определяет, соответствует ли аргумент ожидаемому типу
    //*резольвер запускается перед КАЖДЫМ контроллером, но благодаря этому
    //  методу понимает, к чему применять обработчик, а к чему - нет.
    public function supports(Request $request, ArgumentMetadata $argument): bool
    {
       return ServiceInterface::class == $argument->getType();
    }

    // Производит обработку данных и выбор конкретного значения
    public function resolve(Request $request, ArgumentMetadata $argument): Generator
    {
        if ($request->get('serviceName) == 'first') {
            yield $this->firstService;
        } elseif ($request->get('serviceName) == 'second') {
            yield $this->secondService;
        } else {
            throw new BadrequestHttpException('Неверно указано имя сервиса, serviceName');
    }
}

Интерфейс выглядит просто. Он должен иметь общие методы сервисов, вызываемые в контроллере:

// src/Service/GeoserviceInterface.php
namespace App\Service;

interface ServiceInterface 
{
    /**
     * @param string $address
     * 
     * @return array
     */
    public function getPlaces(string $address): array;
}

Теперь в контроллере не нужно дублировать код и вставлять элсифы:) :

// src/Controller/AddressController.php
    namespace App\Controller;

    use App\Service\ServiceInterface;
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\Annotation\Route;

    public function indexAction(Request $request, ServiceInterface $serviceInterface): array
    {
        return $geoserviceInterface->getPlaces($request->get('address'));
    }

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

// config/services.yaml
services:
    _defaults:
        // ... убедитесь, что автозагрузка сервисов включена
        autowire: true
    // ...

    App\ArgumentResolver\ServiceValueResolver:
        tags: 
            - { name: controller.argument_value_resolver, priority: 150 } 
            // priority лучше поставить > 100, иначе ваш резольвер не запуститься перед деволтным

Готово :) Не совсем просто, зато красиво)

Источники