Symfony, formulaire et DataTransformer

une bonne prise de tête

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Au menu aujourd’hui, un DataTransformer récalcitrant.

Sur le principe, je fais des groupes d’utilisateurs pour qu’ils puissent communiquer entre eux. Entité Group typique avec un 'name' de groupe et une liste de 'members’, des Users (relation ManyToMany). Bon. Si je fais un formulaire Symfony classique avec un champ 'members’ configuré comme un EntityType avec multiple et expanded à true, j’ai des checkboxes, j’envoie le formulaire, ça fonctionne, c’est chouette.

Mon souci, (et je m’en veux tellement d’être perfectionniste à la noix qui cherche à optimiser ce truc) c’est qu’à l’affichage du formulaire il me fait 97 requêtes pour les utilisateurs, et ça, c’est sans que la liste définitive des utilisateurs soit en place. Vu que ce nombre apparait en jaune dans la debug bar de Symfony, je tente d’optimiser le formulaire via un chargement de la liste par Ajax. Mon formulaire perd donc son EntityType et gagne à la place un ChoiceType :

$builder
     ->add('name')            
     ->add('members', ChoiceType::class, [
           'label' => 'Membres',
           'choices' => $options['users'],            
            'multiple' => true,
            'expanded' => true,
     ])
     ->add('save', SubmitType::class, [
           'label' => $options['submit_label']
      ])            
     ->get('members') // Récupérer le champ
     ->addModelTransformer($this->transformer)
;

Il y a donc un DataTransformer comme recommandé par Symfony. (j’ai d’abord testé sans mais S me renvoie "An exception has been thrown during the rendering of a template ("Unable to transform value for property path "members": Expected an array.")."

Voilà le DataTransformer en question :

<?php

namespace App\Form\DataTransformer;

use App\Repository\UserRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class UserToArrayTransformer implements DataTransformerInterface
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Transforme une collection d'utilisateurs en un tableau contenant leurs IDs.
     *
     * @param mixed $users
     * @return array
     */
    public function transform($users): array
    {
        dump('Transform called', $users);
        if (null === $users) {
            return [];
        }

        // Si la collection est un objet Doctrine (ArrayCollection), on la convertit en tableau
        if ($users instanceof \Doctrine\Common\Collections\Collection) {
            $users = $users->toArray();
        }

        // Retourne un tableau des IDs des utilisateurs
        return array_map(static fn($user) => $user->getId(), $users);
    }

    /**
     * Transforme un tableau d'IDs en une collection d'utilisateurs.
     *
     * @param mixed $userIds
     * @return array
     * @throws TransformationFailedException
     */
    public function reverseTransform($userIds): array
    {
        dump('ReverseTransform called', $userIds);

        if (null === $userIds || !is_array($userIds)) {
            return [];
        }

        // Recherche les utilisateurs par leurs IDs
        $users = $this->userRepository->findBy(['id' => $userIds]);

        // Vérifie que tous les IDs correspondent à des utilisateurs
        if (count($users) !== count($userIds)) {
            throw new TransformationFailedException('Certaines valeurs ne correspondent à aucun utilisateur.');
        }

        return $users;
    }
}

Quand je soumets le formulaire, j’ai bien le dump "ReverseTransform called" mais $userIds est []. Pourtant, à l’envoi du formulaire, intercepté en AJAX et mis en forme avec un FormData, j’ai bien dans mon formulaire "group[members][]": Array["6", "12"].

Pour info, quand je teste la même chose avec le formulaire Symfony classique avec EntityType, j’ai bien exactement la même structure de données, et si j’ajoute le DataTransformer dans ce formulaire, il dump deux Users (ceux que j’avais cochés ; oui il les dump avant d’aller les chercher dans la BDD ligne 55 mais j’imagine que c’est parce que ce sont directement des entités).

Est-ce qu’il y a moyen de s’insérer d’une manière ou d’une autre entre l’envoi et le datatransformer pour voir quelles sont les données effectivement reçues ? Je ne trouve pas dans la documentation :honte:

Ou bien je m’y prends comme un manche dans ma démarche ?

Merci merci.

Problème résolu. Le DataTransformer n’y était pour rien. Quelques ajustements sur le GroupType.php :

class GroupType extends AbstractType
{
    public function __construct(private UserToArrayTransformer $transformer) {}

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $this->users = $options['users'];
       
        $builder
            ->add('name')            
            ->add('members', ChoiceType::class, [
                    'label' => 'Membres',
                    'choices' => $this->users,                
                    'attr' => [
                        'class' => 'members-selector', // Classe pour cibler en JS
                    ],
                    'multiple' => true,
                    'expanded' => true,
            ])
            ->add('save', SubmitType::class, [
                'label' => $options['submit_label']
            ])            
            ->get('members') // Récupérer le champ
            ->addModelTransformer($this->transformer)
            ;        
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Group::class,
            'submit_label' => 'Créer le groupe',
            'users' => [],
        ]);
    }
}

Parce que je n’ai pas réussi à avoir le résultat attendu :honte:

Les formulaires Symfony sont une pièce-clé de l’ensemble mais je galère à chaque fois que je sors du schéma "facile" de la simple Entité. J’ai passé un temps considérable hier pour essayer d’optimiser tout ça, j’ai fait des tas de variantes, avec le query_builder, ajax, avec ce DataTransformer, etc. Systématiquement avec le query_builder il faisait un nombre considérable de requêtes alors que j’avais juste besoin de l’id et du fullname. Là ça fonctionne et j’y touche plus ^^'

Ceci dit je dois maintenant appliquer la même technique de DataTransformers avec un nouveau formulaire pour envoyer des messages à un destinataire ou à un groupe, avec un thème particulier. L’ensemble utilise cette même technique des options envoyées au formulaire, pour la liste des destinataires, la liste des groupes, et la liste des thèmes. Ô Joie.

+0 -0

Les requêtes répétées, c’était vraiment pour récupérer les utilisateurs ? Un à un, et entité par entité (sans jointure) ?

Je suis plutôt surpris, il me semblait que Symfony proposait plutôt un ChoiceLoader qu’un DataTransformer, et je me disais surtout que query_builder serait suffisant, voire mis en cache…

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte