Sonata DataMapper y Autowiring

Si tienes una necesidad específica de transformar datos entre el formulario y el registro de tu entidad y piensas que este código de transformación debería ubicarse en un servicio porque tiene más sentido, puedes utilizar el dataMapper utilizando la inyección de dependencias proporcionada por Symfony.

¿Pero cómo implementarlo?

Un pequeño recordatorio sobre el DataMapper. Si estamos de acuerdo con la documentación oficial de Symfony respecto a DataMapper y DataTransformer, un dataMapper tiene la responsabilidad de leer y escribir un objeto (una entidad) para pasarlo a un formulario. En resumen, hace la conexión entre un formulario y su entidad.

Para resumir: El transformador de datos cambia un elemento de la entidad, el dataMapper es responsable del objeto completo.

DataMapperInterface lógicamente impone dos métodos:

Uno toma los datos del formulario para darnos la posibilidad de transformarlos en la entidad objetivo (mapFormsToData()). El otro, inversamente, toma los datos de la entidad para transformarlos para el formulario (mapDataToForms()).

Hay que tener cuidado, esta transformación tiene lugar antes del registro de la entidad, por lo tanto, antes de las validaciones.
Si tienes una necesidad más compleja, puedes utilizar los eventos de Symfony definidos en la clase Symfony\Component\Form\FormEvents. Esto, por cierto, es recomendado.
FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT, FormEvents::PRE_SET_DATA, FormEvents::POST_SET_DATA. Seguramente hablaremos de esto otra vez en un futuro artículo.

Por ahora, volvamos a nuestro formulario. Te mostraremos cómo puedes usar fácilmente las declaraciones de servicios y la autoconfiguración de Symfony para usar un servicio directamente en un dataMapper con Sonata. Nuestro ejemplo no es muy relevante, ya que ya existe un mecanismo mucho más avanzado en Symfony. Usaremos un campo de texto "Title" para escribir su versión "slug" en un campo "slug" automáticamente. Para formatear el Slug, usaremos el servicio que nos ofrece Symfony: Symfony\Component\String\Slugger\SluggerInterface.

Sonata_ejemplo_slug

Por lo tanto, implementaremos un formulario con el único campo 'título'. Al enviar este formulario, pasaremos por el DataMapper que llamará al SlugService, y este servicio tendrá un método slugify() que buscará el servicio Symfony SluggerInterface. Aquí está el esquema de la tabla:

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;

Aquí está la definición de nuestros servicios:

    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

En orden, tenemos el formulario, el servicio Slug y nuestro DataMapper que nos permite hacer la conexión entre los dos. El truco es considerar el dataMapper como un servicio. La inyección de dependencias se realizará de forma natural por Symfony. El dataMapper se aplicará al Formulario que a su vez tendrá su servicio configurado porque se declara en su constructor.

Ahora necesitamos agregar la declaración de nuestro formulario en sonata_admin.yaml.

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

Aquí está el formulario en cuestión:

<?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')
        ;
    }
}

Como puedes ver, título es un campo de texto estándar. Añadimos el campo slug para tener acceso a él en nuestro DataMapper (pero no lo mostramos) y hemos añadido un campo plantilla solo para tener la visualización de nuestro campo slug informado.

Observa el servicio DataMapper pasado en el constructor que reutilizamos directamente en el formBuilder a través de 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;
    }
}

Rellenamos el array $form en el primer método \App\Form\DataMapper\SlugDataMapper::mapDataToForms. En el segundo método \App\Form\DataMapper\SlugDataMapper::mapFormsToData, creamos una entidad con nuestra nueva información para pasar la validación y el registro. El constructor nos proporciona automáticamente nuestro SlugService que a su vez tendrá el servicio Symfony Slug en su constructor.

<?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) ;;
    }

}

Para terminar el ejemplo, el código de nuestro campo "plantilla" que nos permite visualizar el resultado:

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

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

¡Ahí lo tienes!