Créer une annotation de contrôleur

Il arrive que l’on doive modifier des données à la volée (clef et ou valeur), mais rien ne correspond: un dataTransformer va bien me transformer la donnée, mais la clef transmise va rester la même. Dans un contrôleur, c’est vraiment moche de récupérer la request et de la traiter dans le handler de formulaire, même si ça reste possible. Il y a aussi les formEvents, au PRE_SUBMIT, ou au SUBMIT, mais ce n’est pas génial, pas réutilisable. On remplace alors une clef par une autre, une data simple est transformée en objet…

Je me suis retrouvé confronté au fait que je transmettais un body dont le corps ne correspondait pas exactement à celui de l’entité. En effet, j’avais des relations ManyToOne et mes attributs étaient donc $myRelatedEntity et $MyRelatedEntity2. Sauf que dans mon formulaire et dans mon appel json, je ne voulais pas passer my_related_entity et my_related_entity2, car ce n’est pas la réalité. Je transmets un id, pas une entité. Du coup, je voulais que le corps de mon json soit plutôt de la sorte:

{
   my_related_entity_id: "myRelatedEntityKey",
   my_related_entity2_id: "myRelatedEntity2Key",
}

Ok, c’est problématique, car je suis obligé de mapper ces champs à false dans le formulaire, ce qui n’est pas terrible, puis de faire le mapping avec les véritables champs my_related_entity et my_related_entity2.

Et la nécessité d’une annotation de contrôleur s’est alors faite sentir.

Elle intervient au niveau de la récupération du corps de la requête et s’exécute. Pour en créer une, c’est très simple, il suffit de deux fichiers:

  • celui de l’objet Annotation (appelé ici PropertyMapper.php)
  • et un driver que nous allons ici appeler AnnotationDriver.php

Le premier fichier correspond aux attributs de l’annotation:

namespace AppBundle\Annotation;

/**
 * @Annotation
 */
class PropertyMapper
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var string
     */
    public $renamedTo;
}

Le second fait le traitement:

namespace AppBundle\Annotation\Driver;

use AppBundle\Annotation\PropertyMapper;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class AnnotationDriver
{
    /**
     * @var Reader
     */
    private $reader;

    /**
     * AnnotationDriver constructor.
     *
     * @param Reader $reader
     */
    public function __construct(Reader $reader)
    {
        $this->reader = $reader;
    }

    /**
     * @param FilterControllerEvent $event
     *
     * @throws \Exception
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        if (!is_array($controller = $event->getController())) {
            return;
        }

        $object = new \ReflectionObject($controller[0]);
        $method = $object->getMethod($controller[1]);

        foreach ($this->reader->getMethodAnnotations($method) as $configuration) {
            if ($configuration instanceof PropertyMapper) {
                $request = $event->getRequest();
                $parameters = $request->request;

                if (!$parameters->has($configuration->name)) {
                    continue;
                }

                $parameters->set($configuration->renamedTo, $parameters->get($configuration->name));
                $parameters->remove($configuration->name);
            }
        }
    }
}

L’annotationDriver doit prendre en paramètre un reader (ici, j’ai pris celui de doctrine) et l’action se fait sur un onKernelController.

Nous récupérons l’objet traité (ici donc l’entité concernée par l’annotation) et la méthode (par exemple postMyEntity. Il s’agit de la méthode du contrôleur sur laquelle l’entité porte).

Nous bouclons sur toutes les annotations du reader (donc celle de doctrine mais aussi la nôtre). Si l’annotation et du type de celle de nous avons créée (ici PropertyMapper), nous récupérons les paramètres de la requête en POST, et nous changeons le contenu du ParameterBag. Le champ dont la clef est « name » prend la clef « renamedTo ». Et bien sûr, nous supprimons du parameterBag la clef « name » (qui correspond ici à my_related_entity_id).

Enregistrons notre annotation

app.annotation.driver:
        class: AppBundle\Annotation\Driver\AnnotationDriver
        tags: [{name: kernel.event_listener, event: kernel.controller, method: onKernelController}]
        arguments: ['@annotation_reader']

Utilisons notre annotation de contrôleur

    /**
     * @param Request $request
     * 
     * @PropertyMapper(name="myRelatedEntityId", renamedTo="myRelatedEntity")
     * @PropertyMapper(name="myRelatedEntity2Id", renamedTo="myRelatedEntity2")
     */
    public function postMyEntityAction(Request $request)
    {
        [...]
    }

Et voilà! Notre clef est correctement mappée et ce qui est véritablement transmis lors du submit est myRelatedEntity et non myRelatedEntityId! (le camelCase correspond ici aux attributs de l’entité, alors que dans le POST, c’est du snakeCase qui est utilisé)

Rédigé par

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.