Sontata DataMapper et Autowiring

Si vous avez un besoin spécifique de transformation de données entre le formulaire et l’enregistrement de votre entité et que vous pensez que ce code de transformation devrait se trouver dans un service, car cela vous semble plus logique, vous pouvez utiliser le dataMapper en utilisant l’injection de dépendance fournit par Symfony.

Mais alors comment le mettre en oeuvre ?

Un petit rappel sur le DataMapper. Si on s’accorde à la documentation officielle de Symfony concernant les DataMapper et DataTransformer, un dataMapper a la responsabilité de lire et écrire un objet (une entité) pour le passer à un formulaire. En simplifié, il fait la liaison entre un formulaire et son entité.

Pour résumer : Le data transformer change un élément de l’entité, le dataMapper a la responsabilité de l’ensemble de l’objet.

DataMapperInterface nous impose en toute logique deux methodes:

L’une prend les données du formulaire pour nous donner la possibilité de les transformer dans l’entité cible (mapFormsToData()). L’autre, à l’inverse, prend les données de l’entité pour les transformer à destination du formulaire (mapDataToForms()).

Attention, cette transformation se place avant l’enregistrement de l’entité, donc des validations.
Si vous avez un besoin plus complexe vous pouvez utiliser les événements Symfony définis dans la classe Symfony\Component\Form\FormEvents. C’est d’ailleurs ce qui est recommandé.
FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT, FormEvents::PRE_SET_DATA, FormEvents::POST_SET_DATA. Nous aurons certainement l’occasion d’en reparler dans un prochain article.

Pour l’instant revenons à notre formulaire. Nous allons vous montrer comment vous pouvez facilement utiliser les déclarations de service et l’autowiring de Symfony pour utiliser un service directement dans un dataMapper avec Sonata. Notre exemple n’est pas très pertinent, car un mécanisme bien plus poussé existe déjà dans Symfony. Nous allons utiliser un champ texte « Title » pour écrire sa version « slug » dans un champ « slug » automatiquement. Pour formatter le Slug nous allons utiliser le service que Symfony nous propose : Symfony\Component\String\Slugger\SluggerInterface.

Sonata_exemple_slug

Nous allons donc mettre en œuvre un formulaire avec comme unique champ ‘titre‘. Au submit de ce formulaire nous allons passer par le DataMapper qui lui fera appel à un service SlugService qui lui même aura une methode slugify() qui elle ira chercher le service Symfony SluggerInterface. Voici le schéma de la table :

CREATE TABLE `titles` (
  `id` int NOT NULL,
  `title` varchar(350) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `slug` varchar(350) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

La définition de nos services :

    admin.titles:
        class: App\Admin\SlugTitlesAdmin
        tags:
            - { name: sonata.admin, model_class: App\Entity\SlugTitles, controller: App\Controller\SlugTitlesAdminController, manager_type: orm, group: admin , label: "Administrations de vos titres de pages" }
        arguments: [ ~, ~, ~, "@slug.datamapper" ]

    app.slug_service:
        class: App\Service\SlugService

    slug.datamapper:
        class: App\Form\DataMapper\SlugDataMapper
        arguments : ["@app.slug_service"]
        public: true

Nous avons dans l’ordre, le form, le service Slug et notre DataMapper nous permettant de faire la liaison entre les deux. L’astuce est de considérer le dataMapper comme un service. L’injection de dépendance sera réalisée naturellement par Symfony. Le dataMapper sera appliqué au Form qui lui même aura naturellement son service de mis en place car déclaré dans son constructeur.

Il nous faut à présent ajouter dans sonata_addmin.yaml la déclaration de notre formulaire.

    dashboard:
        groups:
            runroom:
            slug_titles:
                label: "Titles"
                icon: <i class="fa fa-users"></i>
                on_top: true
                items:
                    - admin.titles

Voici le formulaire en question :

<?php
declare(strict_types=1);

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\TemplateType;
use Sonata\AdminBundle\Show\ShowMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

final class SlugTitlesAdmin extends AbstractAdmin
{

    private DataMapperInterface $slugDataMapper;

    /**
     * @param string|null $code
     * @param string|null $class
     * @param string|null $baseControllerName
     * @param DataMapperInterface $slugDataMapper
     */
    public function __construct(?string $code, ?string $class,?string  $baseControllerName, DataMapperInterface $slugDataMapper)
    {
        parent::__construct($code, $class, $baseControllerName);
        $this->slugDataMapper = $slugDataMapper;
    }

    /**
     * @param DatagridMapper $filter
     * @return void
     */
    protected function configureDatagridFilters(DatagridMapper $filter): void
    {
        $filter
            ->add('id')
            ->add('title')
            ->add('slug')
        ;
    }

    /**
     * @param ListMapper $list
     * @return void
     */
    protected function configureListFields(ListMapper $list): void
    {
        $list
            ->add('title')
            ->add('slug')
            ->add(ListMapper::NAME_ACTIONS, null, [
                'actions' => [
                    'show' => [],
                    'edit' => [],
                    'delete' => [],
                ],
            ]);
        ;
    }

    /**
     * @param FormMapper $form
     * @return void
     */
    protected function configureFormFields(FormMapper $form): void
    {
        $form
            ->add('title', TextType::class, ['help' => 'Renseignez le titre de votre article'])
            ->add('slug', HiddenType::class)
            ->add('informations',TemplateType::class,  [
                    "template"=>'slug_informations.html.twig',
                    'label' => false,
                    'parameters'=> ['subject'=> $this->getSubject()]
                ]
            )
        ;
        $builder = $form->getFormBuilder();
        $builder->setDataMapper($this->slugDataMapper);
    }

    /**
     * @param ShowMapper $show
     * @return void
     */
    protected function configureShowFields(ShowMapper $show): void
    {
        $show
            ->add('id')
            ->add('title')
            ->add('slug')
        ;
    }
}

Comme vous le voyez, titre est un champ texte classique. Nous ajoutons le champ slug afin d’y avoir accès dans notre DataMapper (mais nous ne l’affichons pas) et nous avons rajouté un champ template juste pour que nous ayons l’affichage de notre champ slug renseigné.

Remarquez le service DataMapper passé dans le constructeur que nous réutilisons directement dans le formBuilder via setDataMapper().

<?php

namespace App\Form\DataMapper;

use App\Entity\SlugTitles;
use App\Service\SlugService;
use Symfony\Component\Form\DataMapperInterface;
use Traversable;

final class SlugDataMapper implements DataMapperInterface
{
    protected SlugService $slugService;

    /**
     * @param SlugService $slugService
     */
    public function __construct(SlugService $slugService){

        $this->slugService = $slugService;
    }

    /**
     * @param mixed $viewData
     * @param Traversable $forms
     * @return void
     */
    public function mapDataToForms(mixed $viewData, Traversable $forms): void
    {
        if(is_null($viewData)){
            return;
        }

        $forms = iterator_to_array($forms);
        $forms['title']->setData($viewData->getTitle());
        $forms['slug']->setData($viewData->getSlug());
    }

    /**
     * @param Traversable $forms
     * @param mixed $viewData
     * @return void
     */
    public function mapFormsToData(Traversable $forms, mixed &$viewData) :void
    {
        $submitedForm = iterator_to_array($forms);
        $formEntity =  new SlugTitles();
        $formEntity->setTitle($submitedForm['title']->getData());
        $formEntity->setSlug($this->slugService->slugify($submitedForm['title']->getData()));
        $viewData = $formEntity;
    }
}

Nous remplissons le tableau $form dans la première méthode \App\Form\DataMapper\SlugDataMapper::mapDataToForms. Dans la seconde méthode \App\Form\DataMapper\SlugDataMapper::mapFormsToData nous créons une entité avec nos nouvelles informations afin de leur faire passer validation et enregistrement. Le constructeur nous fournit automatiquement notre service SlugService qui lui même aura dans son constructeur le service Slug de Symfony.

<?php

namespace App\Service;

use Symfony\Component\String\Slugger\SluggerInterface;

class SlugService
{
    private SluggerInterface $slugger;

    /**
     * @param SluggerInterface $slugger
     */
    public function __construct(SluggerInterface $slugger){
        $this->slugger = $slugger;
    }

    /**
     * @param string $string
     * @return String|null
     */
    public function slugify(string $string): ?String
    {
        return $this->slugger->slug($string) ;;
    }

}

Pour finir l’exemple, le code de notre champ « template » qui nous permet de visualiser le résultat :

<h3>Valeurs de notre entité</h3>

Id: {{ subject.id }} <br />
Title : {{ subject.title }} <br />
Slug : {{ subject.slug }} <br />

Voilà !