symfonysymfony-formsnested-formsarraycollection

Symfony 5.2 - Form - CollectionType - The form's view data is expected to be a "App\Entity\...", but it is a array


I have this error when I try to render the form AvisType:

The form's view data is expected to be a "App\Entity\DTO\DocumentDTO", but it is a "array". You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms "array" to an instance of "App\Entity\DTO\DocumentDTO".

I have an AvisType form:

<?php

namespace App\Form;


use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;

use App\Entity\DTO\AvisDTO;
use App\Entity\DTO\NomenclatureDTO;
use App\Entity\DTO\DataFormMapper\DataAvis;
use App\Entity\DTO\DocumentDTO;

class AvisType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('nomAuteur', TextType::class, [
                'label' => "Nom",
            ])
            ->add('prenomAuteur', TextType::class, [
                'label' => "Prénom"
            ])
            ->add('boEstTacite', ChoiceType::class, [
                'required' => true,
                'label' => "L'avis est-il tacite ?",
                'label_attr' => [
                    'class' => "font-weight-bold"
                ],
                'expanded' => true,
                'multiple' => false,
                'choices' => [
                    'Oui' => true,
                    'Non' => false,
                ],
            ])
            ->add('nomNatureAvisRendu', ChoiceType::class, [
                'label' => "Nature de l'avis rendu",
                'required' => false,
                'choices' => $options['dataPostAvis']->nomNatureAvisRendu,
                'choice_label' => function ($choice) {
                    return $choice->libNom;
                },
                'setter' => function (AvisDTO &$avisDto, NomenclatureDTO $nomenclatureDto) {
                    if ($nomenclatureDto) {
                        $avisDto->nomNatureAvisRendu = $nomenclatureDto->idNom;
                    }
                }
            ])
            ->add('nomTypeAvis', ChoiceType::class, [
                'label' => "Type d'avis",
                'required' => false,
                'choices' => $options['dataPostAvis']->nomTypeAvis,
                'choice_label' => function ($choice) {
                    return $choice->libNom;
                },
                'setter' => function (AvisDTO &$avisDto, NomenclatureDTO $nomenclatureDto) {
                    if ($nomenclatureDto) {
                        $avisDto->nomTypeAvis = $nomenclatureDto->idNom;
                    }
                }
            ])
            ->add('documents', CollectionType::class, [
                'entry_type' => DocumentType::class,
                'entry_options' => [
                    'data' => $options,
                ],
                'prototype' => true,                
                'allow_add' => true,
                'by_reference' => false,                
            ])
            ->add('txAvis', TextareaType::class, [
                'required' => true,
                'attr' => [
                    'placeholder' => "Avis favorable avec prescriptions. \nPremière prescription : Les volets doivent être en bois"
                ]
            ])
            ->add('txFondementAvis', TextareaType::class, [
                'attr' => [
                    'placeholder' => "L'avis de l'ABF est rendu en application de l'article R. 425-30 du Code de l'urbanisme."
                ]
            ])
            ->add('txHypotheses', TextareaType::class, [
                'attr' => [
                    'placeholder' => "Dans l'hypothèse où la puissance électrique nécessaire est de x alors le coût de raccordement est de y"
                ]
            ])
            ->add('txQualiteAuteur', TextareaType::class, [
                'attr' => [
                    'placeholder' => "Qualité"
                ]
            ])
            ->add('Envoyer', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => AvisDTO::class,
            'dataPostAvis' => DataAvis::class,
        ]);
    }
}

This is my AvisDTO class:

<?php

namespace App\Entity\DTO;

use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;

class AvisDTO
{
    public string $dtAvis;
    public string $dtEmission;
    public string $idActeurAuteur;
    public string $nomAuteur;
    public string $prenomAuteur;
    public bool $boEstTacite;
    public ArrayCollection $documents;
    public string $idConsultation;
    public array $idsPieces;
    public int $nomNatureAvisRendu;
    public int $nomTypeAvis;
    public string $txAvis;
    public string $txFondementAvis;
    public string $txHypotheses;
    public string $txQualiteAuteur;


    public function __construct()
    {
        $this->documents = new ArrayCollection();
    }
}

And finally here is my DocumentType:

<?php
namespace App\Form;


use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;

use App\Entity\DTO\DocumentDTO;
use App\Entity\DTO\NomenclatureDTO;

class DocumentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        // dd($options);
        $builder
            ->add('nomTypeDocument', ChoiceType::class, [
                'label' => "Type de document",
                'required' => false,                
                'choices' => $options['data']['dataPostAvis']->nomTypeDocument,
                'choice_label' => function ($choice) {
                    return $choice->libNom;
                },                              
                'setter' => function(DocumentDTO &$documentDto, NomenclatureDTO $nomenclatureDto) {                         
                    if ($nomenclatureDto) {                     
                        $documentDto->nomTypeDocument = $nomenclatureDto->idNom;
                    }
                }               
            ])
            ->add('nomTypeProducteurDoc', ChoiceType::class, [
                'label' => "Type de producteur du document",
                'required' => false,                
                'choices' => $options['data']['dataPostAvis']->nomTypeProducteurDoc,
                'choice_label' => function ($choice) {
                    return $choice->libNom;
                },              
                'setter' => function(DocumentDTO &$documentDto, NomenclatureDTO $nomenclatureDto) {
                    if ($nomenclatureDto) {
                        $documentDto->nomTypeProducteurDoc = $nomenclatureDto->idNom;
                    }
                }
            ])
            ->add('upload_file', FileType::class, [
                'label' => false,
                'mapped' => false,
                'attr' => [
                    'data-dossier-target' => 'fileName',
                    'data-action' => 'change->dossier#getFileName'
                ],
                'constraints' => [
                    new File([
                        'mimeTypes' => [
                            'application/pdf', //PDF
                            'application/msword', //DOC
                            'application/vnd.openxmlformats-officedocument.wordprocessingml.document', //DOCX
                            'application/vnd.oasis.opendocument.spreadsheet', //ODS
                            'application/vnd.oasis.opendocument.text', //ODT
                            'application/vnd.ms-excel', //XLS
                            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', //XLSX
                            'image/bmp', //BMP
                            'image/x-ms-bmp', //BMP
                            'image/png', //PNG
                            'image/gif', //GIF
                            'image/tiff', //TIF & TIFF
                            'image/jpeg', //JPEG & JPG
                            'image/dib', //DIB
                        ],
                        'mimeTypesMessage' => "Ce document n'est pas valide.",
                    ])
                ]
            ])
            ->add('fileId', HiddenType::class)
            ->add('folderId', HiddenType::class)
            ->add('dtProduction', HiddenType::class)
            ->add('idActeurProducteur', HiddenType::class)
            ->add('idsPersonnesProductrices', HiddenType::class);
    }
    
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => DocumentDTO::class,         
            'dataPostAvis' => DataAvisType::class,
        ]);
    }
}

And the DocumentDTO:

<?php

namespace App\Entity\DTO;

class DocumentDTO
{
    public string $fileId;
    public string $folderId;
    public string $dtProduction;
    public string $idActeurProducteur;
    public array $idsPersonnesProductrices;
    public int $nomTypeDocument;
    public int $nomTypeProducteurDoc;
}

My controller:

$avis = new AvisDTO();
$avis->idConsultation = $idConsultation;
$avis->idActeurAuteur = $idActeurAppelant; // A verifier    
$avis->idsPieces = $idPiecesList;        

$form = $this->createForm(AvisType::class, $avis, $options);
$form->handleRequest($request);

So what I understand is that Symfony can't create the DocumentType form because it receives an array from AvisType. I need this nested form to be "dynamic", my user must be able to add new Document to the AvisType.

I've been following this documentation: https://symfony.com/doc/current/form/form_collections.html#allowing-new-tags-with-the-prototype

How shall I proceed to solve this error without setting my DocumentType data_class to null ?


Solution

  • You can add methods in your DocumentDTO class to transform to/from array. If you do this then that class would look something like this:

    <?php
    
    namespace App\Entity\DTO;
    
    class DocumentDTO
    {
        public string $fileId;
        public string $folderId;
        public string $dtProduction;
        public string $idActeurProducteur;
        public array $idsPersonnesProductrices;
        public int $nomTypeDocument;
        public int $nomTypeProducteurDoc;
    
        public static function fromArray($data): self {
            $dto = new self();
            $dto->fileId = $data['fileId'] ?? null;
            $dto->folderId = $data['folderId'] ?? null;
            $dto->dtProduction = $data['dtProduction'] ?? null;
            $dto->idActeurProducteur = $data['idActeurProducteur'] ?? null;
            $dto->idsPersonnesProductrices = $data['idsPersonnesProductrices'] ?? null;
            $dto->nomTypeDocument = $data['nomTypeDocument'] ?? null;
            $dto->nomTypeProducteurDoc = $data['nomTypeProducteurDoc'] ?? null;
    
            return $dto;
        }
    
        public function toArray(): array
        {
            return [
                'fileId' => $this->fileId,
                'folderId' => $this->folderId,
                'dtProduction' => $this->dtProduction,
                'idActeurProducteur' => $this->idActeurProducteur,
                'idsPersonnesProductrices' => $this->idsPersonnesProductrices,
                'nomTypeDocument' => $this->nomTypeDocument,
                'nomTypeProducteurDoc' => $this->nomTypeProducteurDoc,
            ];
        }
    }
    

    In your DocumentType class you can add a viewTransformer in which you use the methods from your dto class to convert to/from the data object. If you do this the App\Form\DocumentType will look something like this:

    <?php
    namespace App\Form;
    
    
    use App\DataObject\PaymentProviderCredentialsDto;
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\CallbackTransformer;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\Validator\Constraints\File;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Component\Form\Extension\Core\Type\FileType;
    use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
    use Symfony\Component\Form\Extension\Core\Type\HiddenType;
    
    use App\Entity\DTO\DocumentDTO;
    use App\Entity\DTO\NomenclatureDTO;
    
    class DocumentType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            // dd($options);
            $builder
                ->add('nomTypeDocument', ChoiceType::class, [
                    'label' => "Type de document",
                    'required' => false,
                    'choices' => $options['data']['dataPostAvis']->nomTypeDocument,
                    'choice_label' => function ($choice) {
                        return $choice->libNom;
                    },
                    'setter' => function(DocumentDTO &$documentDto, NomenclatureDTO $nomenclatureDto) {
                        if ($nomenclatureDto) {
                            $documentDto->nomTypeDocument = $nomenclatureDto->idNom;
                        }
                    }
                ])
                ->add('nomTypeProducteurDoc', ChoiceType::class, [
                    'label' => "Type de producteur du document",
                    'required' => false,
                    'choices' => $options['data']['dataPostAvis']->nomTypeProducteurDoc,
                    'choice_label' => function ($choice) {
                        return $choice->libNom;
                    },
                    'setter' => function(DocumentDTO &$documentDto, NomenclatureDTO $nomenclatureDto) {
                        if ($nomenclatureDto) {
                            $documentDto->nomTypeProducteurDoc = $nomenclatureDto->idNom;
                        }
                    }
                ])
                ->add('upload_file', FileType::class, [
                    'label' => false,
                    'mapped' => false,
                    'attr' => [
                        'data-dossier-target' => 'fileName',
                        'data-action' => 'change->dossier#getFileName'
                    ],
                    'constraints' => [
                        new File([
                            'mimeTypes' => [
                                'application/pdf', //PDF
                                'application/msword', //DOC
                                'application/vnd.openxmlformats-officedocument.wordprocessingml.document', //DOCX
                                'application/vnd.oasis.opendocument.spreadsheet', //ODS
                                'application/vnd.oasis.opendocument.text', //ODT
                                'application/vnd.ms-excel', //XLS
                                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', //XLSX
                                'image/bmp', //BMP
                                'image/x-ms-bmp', //BMP
                                'image/png', //PNG
                                'image/gif', //GIF
                                'image/tiff', //TIF & TIFF
                                'image/jpeg', //JPEG & JPG
                                'image/dib', //DIB
                            ],
                            'mimeTypesMessage' => "Ce document n'est pas valide.",
                        ])
                    ]
                ])
                ->add('fileId', HiddenType::class)
                ->add('folderId', HiddenType::class)
                ->add('dtProduction', HiddenType::class)
                ->add('idActeurProducteur', HiddenType::class)
                ->add('idsPersonnesProductrices', HiddenType::class);
    
            $builder->addViewTransformer(new CallbackTransformer(
                function ($documentDto) {
                    if (!is_array($documentDto)) {
                        return $documentDto;
                    }
    
                    return DocumentDTO::fromArray($documentDto);
                },
                function ($documentDto) {
                    if (!$documentDto instanceof DocumentDTO) {
                        return $documentDto;
                    }
    
                    return $documentDto->toArray();
                }
            ));
        }
    
        public function configureOptions(OptionsResolver $resolver): void
        {
            $resolver->setDefaults([
                'data_class' => DocumentDTO::class,
                'dataPostAvis' => DataAvisType::class,
            ]);
        }
    }
    

    Now every time the form view is loaded the transformer will be called and your data will be transformed in the correct format. You should change the transform methods which I suggested to better suit your project's needs.