Sonata DataMapper and Autowiring

If you have a specific need to transform data between the form and the recording of your entity and think that this transformation code should be located in a service because it makes more sense, you can use the dataMapper by utilizing the dependency injection provided by Symfony.

But how to implement it?

A small reminder about the DataMapper. If we agree with the official Symfony documentation regarding DataMapper and DataTransformer, a dataMapper has the responsibility to read and write an object (an entity) to pass it to a form. In short, it makes the connection between a form and its entity.

To summarize: The data transformer changes one element of the entity, the dataMapper is responsible for the entire object.

DataMapperInterface logically imposes two methods:

One takes the data from the form to give us the possibility to transform it into the target entity (mapFormsToData()). The other, conversely, takes the data from the entity to transform it for the form (mapDataToForms()).

Be careful, this transformation takes place before the entity's recording, hence before validations.
If you have a more complex need, you can use the Symfony events defined in the class Symfony\Component\Form\FormEvents. This is, by the way, recommended.
FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT, FormEvents::PRE_SET_DATA, FormEvents::POST_SET_DATA. We will certainly talk about this again in a future article.

For now, let's return to our form. We will show you how you can easily use service declarations and Symfony's autowiring to use a service directly in a dataMapper with Sonata. Our example is not very relevant, as a much more advanced mechanism already exists in Symfony. We will use a "Title" text field to write its "slug" version in a "slug" field automatically. To format the Slug, we will use the service that Symfony offers us: Symfony\Component\String\Slugger\SluggerInterface.

Sonata_exemple_slug

Therefore, we will implement a form with the single field 'title'. Upon the submission of this form, we will go through the DataMapper which will call on the SlugService, and this service will have a slugify() method that will fetch the Symfony SluggerInterface service. Here is the schema of the 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;

Here is the definition of our 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

In order, we have the form, the Slug service, and our DataMapper allowing us to make the connection between the two. The trick is to consider the dataMapper as a service. Dependency injection will be carried out naturally by Symfony. The dataMapper will be applied to the Form which itself will naturally have its service set up because it is declared in its constructor.

We now need to add the declaration of our form in sonata_admin.yaml.

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

Here is the form in 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')
        ;
    }
}

As you can see, title is a standard text field. We add the slug field to have access to it in our DataMapper (but we do not display it) and we've added a template field just so we have the display of our informed slug field.

Notice the DataMapper service passed in the constructor which we directly reuse in the 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;
    }
}

We fill in the $form array in the first method \App\Form\DataMapper\SlugDataMapper::mapDataToForms. In the second method \App\Form\DataMapper\SlugDataMapper::mapFormsToData, we create an entity with our new information to pass validation and recording. The constructor automatically provides us with our SlugService which in turn will have the Symfony Slug service in its 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) ;;
    }

}

To finish the example, the code of our "template" field that allows us to visualize the result:

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

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

There you go!