Les ValueResolver

Description Sommaire

Dans Symfony, les controllers sont au cœur de la logique de notre application. Les méthodes peuvent recevoir des paramètres qui sont automatiquement par résolue par le système de Value Resolver. Ce système peut aussi être étendu avec des attributs pour rajouter de la logique supplémentaire.

// ...
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\Routing\Attribute\Route;

class BlogController
{
    #[Route("/post/{$post}")]
    public function create(
        #[MapEntity] Post $post
        #[MapRequestPayload] CreatePostDTO $data,
    ): Response {

    }
}

Ce système peut être étendu pour créer votre propre système de résolution de paramètre. Pour cela il suffit de créer une classe qui implémente l'interface Symfony\Component\HttpKernel\Controller\ValueResolverInterface.

Exemple

Par exemple, voici un exemple de ValueResolver que j'utilise pour fusionner la logique d'un MapEntity et d'un MapRequestPayload.

Pour commencer je crée un attribut qui me permettra de marquer le paramètre de mon controller comme hydratable.

<?php

namespace App\Http\ValueResolver\Attribute;

use App\Http\ValueResolver\EntityHydratorValueResolver;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Validator\Constraints\GroupSequence;

/**
 * Attribut permettant une fusion entre MapEntity & MapRequestPayload
 * - Récupère l'entité depuis la base de donnée (comme MapEntity)
 * - Injecte les données provenant de la requête
 * - Valide les données.
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final readonly class MapHydratedEntity extends ValueResolver
{

    public function __construct(
        public array $groups = [],
        public string|GroupSequence|array|null $validationGroups = null,
    ) {
        parent::__construct(EntityHydratorValueResolver::class);
    }
}

Ensuite, on crée un ValueResolver qui ne déclenchera sa logique que pour les paramètres avec l'attribut MapHydratedEntity. Cette classe doit implémenter une méthode resolve() qui devra renvoyer un tableau contenant l'élément à injecter dans le controller (ou renvoyer un tableau vide pour passer au ValueResolver suivant.

<?php

namespace App\Http\ValueResolver;

use App\Http\ValueResolver\Attribute\MapHydratedEntity;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Récupère une entité (comme MapEntity), l'hydrate et la valide
 * Agit comme une combinaison entre MapEntity & MapRequestPayload.
 */
final readonly class EntityHydratorValueResolver implements ValueResolverInterface
{
    private EntityValueResolver $entityValueResolver;

    public function __construct(
        private SerializerInterface $serializer,
        private ValidatorInterface $validator,
        ManagerRegistry $registry,
        ?ExpressionLanguage $expressionLanguage = null,
    ) {
        $this->entityValueResolver = new EntityValueResolver($registry, $expressionLanguage);
    }

    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        // On ne s'active que sur les paramètres avec l'attribut MapHydratedEntity
        $attribute = $argument->getAttributes(MapHydratedEntity::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;

        if (!($attribute instanceof MapHydratedEntity)) {
            return [];
        }

        // Agit comme un MapEntity
        $entity = $this->entityValueResolver->resolve($request, $argument)[0] ?? null;
        if (!$entity::class || $entity::class !== $argument->getType()) {
            throw new NotFoundHttpException(sprintf('"%s" object not found by "%s".', $argument->getType(), self::class));
        }

        // Hydrate l'objet avec le contenu de la requête
        $this->serializer->deserialize($request->getContent(), $entity::class, $request->getContentTypeFormat() ?? 'json', [
            'groups' => $attribute->groups,
            AbstractNormalizer::OBJECT_TO_POPULATE => $entity,
        ]);
        $violations = $this->validator->validate($entity, groups: $attribute->validationGroups);

        if (\count($violations)) {
            throw HttpException::fromStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($entity, $violations));
        }

        return [
            $entity,
        ];
    }
}
Publié
Technologies utilisées
Auteur :
Grafikart
Partager