1

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?

1 Answer 1

2

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.

2
  • Thanks for this feedback, indeed I had seen it in the doc but did not understand it that way. This solution works, but it means that if I make several resolvers I will have to do this test each time because they will all be loaded.... If the Symfony doc proposes it like this it is because it is the right way to do it. Thanks in any case. Commented Oct 7 at 11:26
  • You are welcome. I am not entirely sure about how the resolver argument of #[MapRequestPayload] works, but since you are passing in the resolver, you might try removing the tag from services.yaml entirely. That would tell the framework not to use this when resolving route arguments, but passing it into MapRequestPayload could still work. Haven't tried it myself though. The other thing is making sure, that you in fact need those resolvers and whether you should be handling these things in another layer. Commented Oct 8 at 20:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.