8

[UPDATED]: 2019/06/24 - 23;28

Uploading a file with a form, I encounter the following error:

This value should be of type string

The form builder is set to FileType as it should:

FormType

class DocumentType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        /** @var Document $salle */
        $document=$options['data']; //Unused for now
        $dataRoute=$options['data_route']; //Unused for now

        $builder->add('nom')
                ->add('description')
                ->add('fichier', FileType::class, array(
                    //'data_class' is not the problem, tested without it.
                    //see comments if you don't know what it does.
                    'data_class'=>null,
                    'required'=>true,
                ))
                ->add('isActif', null, array('required'=>false));
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults([
            'data_class'=>Document::class,
            'data_route'=>null,
        ]);
    }
}

And my getter and setter have no type hint to make sure that UploadedFile::__toString() won't be invoked:

Entity

class Document {
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=100)
     */
    private $nom;
    /**
     * @ORM\Column(type="string", length=40)
     */
    private $fichier;
    /**
     * @ORM\Column(type="boolean")
     */
    private $isActif;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Salle", inversedBy="documents")
     * @ORM\JoinColumn(onDelete="CASCADE")
     */
    private $salle;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Stand", inversedBy="documents")
     * @ORM\JoinColumn(onDelete="CASCADE")
     */
    private $stand;

    public function __construct() {
        $this->isActif=true;
    }

    public function __toString() {
        return $this->getNom();
    }

    public function getId(): ?int {
        return $this->id;
    }

    public function getNom(): ?string {
        return $this->nom;
    }

    public function setNom(string $nom): self {
        $this->nom=$nom;

        return $this;
    }

    public function getFichier()/*Removed type hint*/ {
        return $this->fichier;
    }

    public function setFichier(/*Removed type hint*/$fichier): self {
        $this->fichier=$fichier;

        return $this;
    }

    public function getIsActif(): ?bool {
        return $this->isActif;
    }

    public function setIsActif(bool $isActif): self {
        $this->isActif=$isActif;

        return $this;
    }

    public function getSalle(): ?Salle {
        return $this->salle;
    }

    public function setSalle(?Salle $salle): self {
        $this->salle=$salle;

        return $this;
    }

    public function getStand(): ?Stand {
        return $this->stand;
    }

    public function setStand(?Stand $stand): self {
        $this->stand=$stand;

        return $this;
    }
}

Yet, form validator is still expecting a string and not an UploadedFile object.

Controller

/**
 * @Route("/dashboard/documents/new", name="document_new", methods={"POST"})
 * @Route("/dashboard/hall-{id}/documents/new", name="hall_document_new", methods={"POST"})
 * @Route("/dashboard/stand-{id}/documents/new", name="stand_document_new", methods={"POST"})
 * @param Router $router
 * @param Request $request
 * @param FileUploader $fileUploader
 * @param SalleRepository $salleRepository
 * @param Salle|null $salle
 * @param Stand|null $stand
 * @return JsonResponse
 * @throws Exception
 */
public function new(Router $router, Request $request, FileUploader $fileUploader, SalleRepository $salleRepository, Salle $salle=null, Stand $stand=null) {
    if($this->isGranted('ROLE_ORGANISATEUR')) {
        $route=$router->match($request->getPathInfo())['_route'];
        if(($route == 'hall_document_new' && !$salle) || ($route == 'stand_document_new' && !$stand)) {
            //ToDo [SP] set message
            return $this->json(array(
                'messageInfo'=>array(
                    array(
                        'message'=>'',
                        'type'=>'error',
                        'length'=>'',
                    )
                )
            ));
        }

        $document=new Document();
        if($route == 'hall_document_new') {
            $action=$this->generateUrl($route, array('id'=>$salle->getId()));
        } elseif($route == 'stand_document_new') {
            $action=$this->generateUrl($route, array('id'=>$stand->getId()));
        } else {
            $action=$this->generateUrl($route);
        }
        $form=$this->createForm(DocumentType::class, $document, array(
            'action'=>$action,
            'method'=>'POST',
            'data_route'=>$route,
        ));

        $form->handleRequest($request);
        if($form->isSubmitted()) {
            //Fail here, excepting a string value (shouldn't), got UploadedFile object
            if($form->isValid()) {
                if($route == 'hall_document_new') {
                    $document->setSalle($salle);
                } elseif($route == 'stand_document_new') {
                    $document->setStand($stand);
                } else {
                    $accueil=$salleRepository->findOneBy(array('isAccueil'=>true));
                    if($accueil) {
                        $document->setSalle($accueil);
                    } else {
                        //ToDo [SP] set message
                        return $this->json(array(
                            'messageInfo'=>array(
                                array(
                                    'message'=>'',
                                    'type'=>'',
                                    'length'=>'',
                                )
                            )
                        ));
                    }
                }

                /** @noinspection PhpParamsInspection */
                $filename=$fileUploader->uploadDocument($document->getFichier());
                if($filename) {
                    $document->setFichier($filename);
                } else {
                    //ToDo [SP] set message
                    return $this->json(array(
                        'messageInfo'=>array(
                            array(
                                'message'=>'',
                                'type'=>'error',
                                'length'=>'',
                            )
                        )
                    ));
                }

                $entityManager=$this->getDoctrine()->getManager();
                $entityManager->persist($document);
                $entityManager->flush();

                return $this->json(array(
                    'modal'=>array(
                        'action'=>'unload',
                        'modal'=>'mdcDialog',
                        'content'=>null,
                    )
                ));
            } else {
                //ToDo [SP] Hide error message
                return $this->json($form->getErrors(true, true));
                // return $this->json(false);
            }
        }

        return $this->json(array(
            'modal'=>array(
                'action'=>'load',
                'modal'=>'mdcDialog',
                'content'=>$this->renderView('salon/dashboard/document/new.html.twig', array(
                    'salle'=>$salle,
                    'stand'=>$stand,
                    'document'=>$document,
                    'form'=>$form->createView(),
                )),
            )
        ));
    } else {
        return $this->json(false);
    }
}

services.yaml

parameters:
    locale: 'en'
    app_locales: en|fr
    ul_document_path: '%kernel.root_dir%/../public/upload/document/'

services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            $locales: '%app_locales%'
            $defaultLocale: '%locale%'
            $router: '@router'

    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    App\Listener\kernelListener:
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
            - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
            - { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

    App\Service\FileUploader:
        arguments:
            $ulDocumentPath: '%ul_document_path%'
Preciel
  • 2,666
  • 3
  • 20
  • 45
  • I'm having this exact issue right now, similar code to OP's – Martijn Jun 20 '19 at 17:51
  • 1
    Please show us the controller that handles the form and parameters from config/services.yaml, thanks. – Arleigh Hix Jun 21 '19 at 21:05
  • updated @ArleighHix – Preciel Jun 21 '19 at 23:29
  • And the App\Service\FileUploader as well please – Arleigh Hix Jun 22 '19 at 03:03
  • @ArleighHix nothing much beside a function to generate a filename, and a function to move the file in the right folder. Not needed for this question, the bug occur before even calling one of the functions in the FileUploader service. – Preciel Jun 22 '19 at 07:16
  • why this line `'data_class'=>null` in form type? – Arleigh Hix Jun 22 '19 at 09:33
  • @ArleighHix When loading a form to edit an already persisted item, the form still expects a `File` instance. You could of course regenerate the file with `new File()`,but it's not really needed. Setting `data_class` to `null` will make it so you don't need to regenerate the file on edit. – Preciel Jun 22 '19 at 19:50

2 Answers2

6

In config/packages/validator.yaml comment out these lines if they exist:

framework:
    validation:
        # Enables validator auto-mapping support.
        # For instance, basic validation constraints will be inferred from Doctrine's metadata.
        #auto_mapping:
        #  App\Entity\: []

See Symfony 4.3 issue [Validation] Activate auto-mapped validation via an annotation #32070.

Brent Pfister
  • 495
  • 6
  • 14
  • 1
    Fixed it for me :) – Martijn Jun 20 '19 at 20:04
  • It's on edit, my problem is on create. And `Assert` is what it's... Read more about `Assert`, it doesn't match my needs. Plus I'm not restricted to one type of file. Last, you can see `'data_class'=>null,` in my formType. It's meant to avoid the problem of recreating the file object on edit. – Preciel Jun 20 '19 at 20:15
  • 1
    To clarify, here my problem is, while I did set `FileType` in my form builder, it's still excepting a `string`, which shouldn't be the case. – Preciel Jun 20 '19 at 20:25
  • I removed the Assert annotations from my code and it still works. I thought I had originally added the ``Assert\Type(type="File"`` after getting an exception. Sorry I don't know how it works. I don't know why the annotations seemingly fixed the problem for @Martijn. – Brent Pfister Jun 22 '19 at 11:02
  • Weird. My only change was adding the assets. After you clear your cache, does it still work? – Martijn Jun 22 '19 at 13:32
  • @Martijn I cleared my cache using ``php bin/console cache:clear`` and refreshed my browser and file upload still works without Assert annotations. I am using Symfony 4.3.1 and PHP v7.2.19-0ubuntu0.18.04.1. Which versions are you using? Could you try removing the Asserts from your entity and check if the exception reoccurs? – Brent Pfister Jun 22 '19 at 15:35
  • I'm not getting the "should be string" when I remove it, it all appears to be working, yet it does nothing :) S4.3.1, php7.2.4, windows10 via WAMP – Martijn Jun 22 '19 at 17:36
  • @Preciel Even though it does not seem necessary and does not meet your needs, could you add the Assert annotations as described in my answer above? Does the exception still occur? If not, try removing those Assert annotations and see if the exception happens? – Brent Pfister Jun 23 '19 at 04:04
  • I tried, doesn't change the problem, still got the error. Somehow, to be excepted... Assert (Constraints) is meant to assert, check, validate, etc... Not to convert. Not sure how it solved @Martijn problem though... – Preciel Jun 23 '19 at 05:15
  • @Preciel Does your ``config/packages/validator.yaml`` contain the ``auto_mapping`` lines like in my edited answer? I was able to reproduce your ``This value should be of type string`` exception by adding those lines to my validator.yaml and removing the Assert annotations on ``$fichier``. – Brent Pfister Jun 25 '19 at 23:16
  • Nice find. Removed validator `auto_mapping` and now it's working. You should clean your answer to leave only the part about `auto_mapping` (and the source links). I think you misunderstand the use of `Assert`... ;) – Preciel Jun 26 '19 at 02:14
  • @Preciel Cleaned answer per your direction. – Brent Pfister Jun 26 '19 at 12:09
2

In your form builder, you set data_class to null:

->add('fichier', FileType::class, array(
    'data_class'=>null,
    'required'=>true,
))

But FileType actually expects some data class to be defined internally. It has some logic to define class dynamically: it is either Symfony\Component\HttpFoundation\File\File for single file upload or null for multiple files.

So, you're effectively force your file control to be multifile, but target field type is string. Symfony does some type-guessing and chooses controls accordingly (e.g. boolean entity field will be represented by checkbox) -- if you don't specify explicit control type and options.

So, I think you should remove data_class from your options, and that will solve the problem.

Here is a link to specific place to make it behave like I have described: https://github.com/symfony/form/blob/master/Extension/Core/Type/FileType.php#L114

As you can see, it decides on data_class value and some other values, then does setDefaults(), i.e. these correct values are there -- unless you override them. A bit fragile architecture, I'd say, but that's what we have to work with.

alx
  • 2,314
  • 2
  • 18
  • 22
  • I did a minimal project to reproduce your issues, but it just works (actually, `data_class` does not affect results making my answer incorrect, and `required` also is irrelevant). But again, it's just works. I have only two clues ATM: 1) `data_route` -- what is it? your form extension? some standard component? standard FormType has no such option; 2) `action` -- are you sure it points to exactly this controller? All in all, I'm happy to share my sample project, though it will look pretty much like https://symfony.com/doc/current/controller/upload_file.html with your class names. – alx Jun 24 '19 at 12:12
  • `data_route` is just a string, I use it to know from where I come so I can adjust the form (I won't have the same fields when editing). `action` is right, it's using une of the 3 routes available for this controller (I updated my code) – Preciel Jun 24 '19 at 12:27
  • OK that's not it, then. Again, my sample project works fine and its code looks close to yours. Some more questions: 1) what symfony version you're using? 2) are there any custom form extension classes in you projects? 3) any model mappers or data transformers in your `DocumentType`? 4) any special options on forms or form fields, like `inherit_data`, `mapped`, etc? – alx Jun 24 '19 at 12:46
  • 1) Symfony 4 (tagged), 2) none, 3) none, 4) no – Preciel Jun 24 '19 at 13:31
  • Can you specify exact version of Symfony, like output from ` composer show | grep symfony/framework` (that would be 4.3.1 for me). Downgrading to something like 4.0 would be one hell of a task... – alx Jun 24 '19 at 13:55
  • I'm using Symfony 4.3.1 as well, updated last night, though it might be a problem with Symfony itself – Preciel Jun 24 '19 at 14:00
  • So, I have reproduced your example as close as I could (given all code I see in your question), and it works just fine. Apparently the problem is in the parts I don't see? No other ideas for now, sorry. – alx Jun 24 '19 at 14:11
  • I will just "roll back" and try again, I might have missed something. There is nothing much that isn't show, the project itself is quite fresh, not much was done. Might just be unlucky. – Preciel Jun 24 '19 at 14:30
  • If it is not large/sensitive, could you share full code somehow? – alx Jun 24 '19 at 14:33
  • Updated my question. Let me know if you need something more. – Preciel Jun 24 '19 at 18:45
  • I did another test from scratch, did not generate entities etc., but pasted your code (commented out some unrelated things like SalleRepository), still it works just fine. (Your code seems to be from different versions of your app, as DocumentType refers to fields that do not exist in Document). – alx Jun 24 '19 at 19:08
  • The only notable difference I could think of is this: this controller action accepts `POST`, but I don't see how your initial form is created. TBH I can't see how it can cause problems, but technically we've been checking only accepting part. Maybe this will give you some clues. – alx Jun 24 '19 at 19:10
  • 1
    Another possible clue: "This value should be of type" string exists only in `symfony/validator` sources. So, unless you've added this exactly string somewhere in your code, this comes from some constraint. Maybe look for 'Constraint' namespace usage in your code? – alx Jun 24 '19 at 19:12
  • Sorry for the form missmatch, it's just that I cleaned irrevelent parameters. About my form, it's the basic `form_start, form_widget, form_end`. I did not edit that part yet. And the only 'Constraint' I have is in the user entity (unique e-mail). I'm at loss at well... :/ – Preciel Jun 24 '19 at 21:32
  • Let me know if you decide to share your project (publicly or privately with me). – alx Jun 24 '19 at 21:40