phpsymfonyconstraints

Validate symfony 4 formfield filetype with mapped false and multiple true


I got a project about snowboard tricks. In my project, I got an entity Trick, that itslef has an attribute "illustrations" that is a collection of another entity : Illustration. I'm trying to do a form TrickType in which the user could load many illustrations (images : png, jpeg, jpg, gif) at once. Then the controller save the files, and several illustration entities are created with the names and paths of the files as "url" attribute.

So, the TrickType can't manage directly illustration entities, because I got to use a FileType field in my form this way the files could be load, and filetype of course doesn't directly create entities. That's why the filetype field in my tricktype form has to have the following options :

My entities, forms and controllers are correctly done : if I don't do any verifications about the files, or if I do verifications, but on only one file, everything perfectly works. But I got to ensure each file is really an image.

Here is what I tried to do so :

1 - constraints directly in TrickType :

->add('illustrations', FileType::class, [
                "label" => "Illustrations (optionnel)",
                "multiple" => true,
                "mapped" => false,
                "required" => false,
                "constraints" => [
                    new File([
                        "maxSize" => "10M",
                        "mimeTypes" => [
                            "image/png",
                            "image/jpg",
                            "image/jpeg",
                            "image/gif"
                        ],
                        "mimeTypesMessage" => "Veuillez envoyer une image au format png, jpg, jpeg ou gif, de 10 mégas octets maximum"
                    ])
                ]
            ])

problem : constraint try to check the field, which is a collection and not a file. So I got the error "this value should be of type string"

2 - no constraint in the form, but in entities

Note : this solution and the following ones wouldn't have prevent wrong files to be save because it only concern entities, not files. So even if it worked, it would just prevent wrong files to be found from url in database and put in the website, but it wouldn't have been a really good solution anyway

in Illustration entity :

/**
     * @ORM\Column(type="string", length=255)
     * @Assert\Regex("/((.jpg)|(.jpeg)|(.png)|(.gif))$/")
     */
    private $url;

in Trick entity, 2 things tried :

/**
     * @ORM\OneToMany(targetEntity="App\Entity\Illustration", mappedBy="trick")
     * @Assert\All({
     *      @Assert\Valid
     * })
     */
    private $illustrations;

error : "The constraint Valid cannot be nested inside constraint Symfony\Component\Validator\Constraints\All"

Second try for tricks :

/**
     * @ORM\OneToMany(targetEntity="App\Entity\Illustration", mappedBy="trick")
     * @Assert\Valid
     */
    private $illustrations;

no errors, but the form is considered valid whatever happen (I can add any type of files, symfony doesn't stop me)

3 - none of what I did before, but callback instead in Trick entity

/**
     * @Assert\Callback
     */
    public function checkIllustrations(ExecutionContextInterface $context, $payload)
    {
        $forbidenExtensions = false;

        foreach ($this->illustrations as $illustration) {
            $url = $illustration->getUrl();
            if (! preg_match('/((.jpg)|(.jpeg)|(.png)|(.gif))$/', $url))
                $forbidenExtensions = true;
        }

        if ($forbidenExtensions)
        {
            $context->buildViolation("L'un des fichiers semble ne pas être une image. Seuls les extensions jpg, jpeg, png et gif sont acceptés.")
                    ->atPath("illustrations")
                    ->addViolation();
        }
    }

This, like in the previous case when I only used Valid (not within All constraint) doesn't do anything. Just like if nothing is checked.

Adding

dump($this->illustrations);
die();

at the beginning of my callback give me a result : an empty arraycollection. And of course, I send some files.

So, The callback is executed, but without illustrations.

This make me wonder if the callback is executed directly when I try to submit the form (so the empty illustrations is normal : there's no illustrations before the controller create it)

For informations, this is my method that handle form submission in my controller :

    /**
     * @Route("/adding-trick", name="adding_trick")
     * @IsGranted("ROLE_USER")
     */
    public function addingTrick(Request $request, ObjectManager $manager)
    {
        $trick = new Trick();

        $form = $this->createForm(TrickType::class, $trick);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $trick->setEditeur($this->getUser());

            $illustrations = $form['illustrations']->getData();

            if($illustrations)
            {
                foreach ($illustrations as $illustrationFile) {
                    $folder = "illustrations";
                    $extension = $illustrationFile->guessExtension();
                    if(!$extension)
                        $extension = "bin";
                    $illustrationName = rand(1, 999999999);
        
                    $illustrationFile->move($folder, $illustrationName . "." . $extension);

                    $illustration = new Illustration();

                    $illustration->setUrl("/" . $folder . "/" . $illustrationName . "." . $extension)
                                 ->setAlt("une illustration de la figure " . $trick->getNom())
                                 ->setTrick($trick);

                    $manager->persist($illustration);
        
                    $figure->addIllustration($illustration);
                }
            }

            // some code for other fields

            $manager->persist($figure);
            $manager->flush();

            $this->addFlash("success", "Figure ajoutée avec succès");

            return $this->redirectToRoute("trick_display", [
                "slug" => $trick->getSlug()
            ]);
        }

        return $this->render('handlingTricks/addingTrick.html.twig', [
            "form" => $form->createView()
        ]);
    }

if somebody has a clue about what I got to do ?


Solution

  • The solution was given jakumi in comments below my first message. Those are my changes :

    As in first attempt, but with a modification (new All contains new File) :

    ->add('illustrations', FileType::class, [
                    "label" => "Illustrations (optionnel)",
                    "multiple" => true,
                    "mapped" => false,
                    "required" => false,
                    "constraints" => [
                        new All([
                            new File([
                                "maxSize" => "10M",
                                "mimeTypes" => [
                                    "image/png",
                                    "image/jpg",
                                    "image/jpeg",
                                    "image/gif"
                                ],
                                "mimeTypesMessage" => "Veuillez envoyer une image au format png, jpg, jpeg ou gif, de 10 mégas octets maximum"
                            ])
                        ])
                    ]
                ])