Search code examples
phpsymfonysymfony7

Symfony 7.X Custom Resolver Always Called


I am developing an API with Symfony 7.1

I decided to use DTO objects for processing and validating the parameters of my requests.

To detect and report if a mandatory parameter is not present in the request, I created a custom resolver.

POST request

curl --location 'http://dev.myproject/api/v1/authentication/user' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data-raw '{
    "username" : "username",
    "password": "myPassword"
}'

The Controller

#[Route('/user', name: 'auth_user', methods: ['POST'], format: 'json')]
    public function authUser(
        #[MapRequestPayload(
            resolver: ApiAuthUserResolver::class
        )] ApiAuthUserDto $apiAuthUserDto,
    ): JsonResponse
    {
      [...]
    }

The DTO object

namespace App\Dto\Api\Authentication;

use Symfony\Component\Validator\Constraints as Assert;

readonly class ApiAuthUserDto
{
    public function __construct(

        #[Assert\Type(
            type : 'string',
            message: 'Le champ username doit être du type string'
        )]
        #[Assert\NotBlank(message: 'Le champ username ne peut pas être vide')]
        public string $username,

        #[Assert\Type(
            type : 'string',
            message: 'Le champ password doit être du type string'
        )]
        #[Assert\NotBlank(message: 'Le champ password ne peut pas être vide')]
        public string $password,
    )
    {
    }
}

The Custom Resolver

namespace App\Resolver\Api;

use App\Dto\Api\Authentication\ApiAuthUserDto;
use App\Utils\Api\ApiParametersParser;
use App\Utils\Api\ApiParametersRef;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Validator\Validator\ValidatorInterface;

readonly class ApiAuthUserResolver implements ValueResolverInterface
{
    public function __construct(
        private ApiParametersParser $apiParametersParser,
        private ValidatorInterface  $validator
    ) {
    }

    /**
     * @param Request $request
     * @param ArgumentMetadata $argument
     * @return iterable
     */
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        $data = json_decode($request->getContent(), true);
        $return = $this->apiParametersParser->parse(ApiParametersRef::PARAMS_REF_AUTH_USER, $data);

        if (!empty($return)) {
            throw new HttpException(Response::HTTP_FORBIDDEN,implode(',', $return));
        }

        $dto = new ApiAuthUserDto(
            $data['username'],
            $data['password']
        );

        $errors = $this->validator->validate($dto);
        if (count($errors) > 0) {
            $nb = $errors->count();
            $msg = [];
            for ($i = 0; $i < $nb; $i++) {
                $msg[] = $errors->get($i)->getMessage() . ' ';
            }
            throw new HttpException(Response::HTTP_FORBIDDEN,implode(',', $msg));
        }
        return [$dto];
    }
}

I set the priority so as not to have problems with other resolvers

  App\Resolver\Api\ApiAuthUserResolver:
    tags:
      - controller.argument_value_resolver:
          priority: 50

The code works well and does its job correctly.

My problem is this:

Since I implemented this custom resolver for my API, all the routes in my applications are broken because my custom resolver is systematically called for a reason I don't know.

For example this code, a very simple route from my project which calls 2 objects

    #[Route('/dashboard/index', name: 'index')]
    #[Route('/dashboard', 'index_3')]
    #[Route('/', name: 'index_2')]
    public function index(DashboardTranslate $dashboardTranslate, UserDataService $userDataService): Response
    {[...]}

Now gives me the following error:

App\Utils\Api\ApiParametersParser::parse(): Argument #2 ($apiParameters) must be of type array, null given, called in \src\ValueResolver\Api\ApiAuthUserResolver.php on line 36

I don't understand why it is systematically my custom resolver that is called even if it has a lower priority and I define it just for an action via the resolver property of MapRequestPayload

What I want to do is that this custom resolver is only used in this specific case and that for classic cases it is the Symfony resolvers that work as it was the case before

Did I forget something, misconfigure my custom resolver?


Solution

  • I suppose it has to do with the tag

    - controller.argument_value_resolver:
          priority: 50
    

    that you added. This basically tells symfony - if you encounter an argument in route, try to resolve it with argument value resolver of highest priority, that supports it.

    The last part is what is important. You need a way to tell te framework, that it cannot possibly use your ValueResolver for anything else but the AuthUser. As declared in the docs, you can achieve this multiple ways.

    I would suggest you first try, if this is in fact the issue. You can specify an initial condition inside your ApiAuthUserResolver::resolve.

    This condition would look something like this:

    public function resolve(Request $request, ArgumentMetadata $argument): iterable
        {
            $argumentType = $argument->getType();
            if (
                !$argumentType
                || !is_subclass_of($argumentType, ApiAuthUserDto::class, true)
            ) {
                return [];
            }
    
            // Rest of the function body as specified in question
        }
    

    Returning empty array tells symfony to use a different value resolver, with lower priority. Hope this helps.