Vichuploaderbundle: Compound forms expect an array or NULL on submission.

Created on 27 Sep 2017  路  13Comments  路  Source: dustin10/VichUploaderBundle

Hi,

I'm getting a validation error when submitting a symfony form with a VichFileType file upload, namely a TransformationFailedException with message "Compound forms expect an array or NULL on submission.".

Now as far as I could tell this is being caused by the fact that the UploadedFile is passed to the VichFileType form as a single element instead of an array. Apparently this transformation (from single element to array) is supposed to be handled by the FileTransformer class of the bundle. These transformer classes are called during the setData method of the symfony form component. This happens only once when creating the form. At this point there is no file yet, so it doesn't take effect. Not sure where I'm going wrong, maybe someone can hint me in the right direction?

The symfony form:

class SomeType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('clientCertificatePath')
            ->add('clientCertificatePw')
            ->add('clientCertificateFile', VichFileType::class, []);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => SomeEntity::class
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'somePrefix';
    }
}

The controller:

        $form = $this->createForm(
            SomeType::class,
            $someEntity,
            [
                'method' => 'POST',
                'csrf_protection' => false,
                'allow_extra_fields' => true
            ]
        );
        $form->handleRequest($request);

The entity:

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\SomeRepository")
 * @Vich\Uploadable()
 */
class SomeEntity extends AnotherEntity
{
    /**
     * @var string
     * @ORM\Column(type="string", nullable=true)
     */
    private $clientCertificatePath;

    /**
     * @var UploadedFile
     * @Vich\UploadableField(mapping="api_certificate", fileNameProperty="clientCertificatePath")
     */
    private $clientCertificateFile;

    /**
     * @var string
     * @ORM\Column(type="string", length=64, nullable=true)
     */
    private $clientCertificatePw;
Support feedback needed

Most helpful comment

In fact the problem that VichFileType::class generate an input that looks like this
<input type="file" id="file_file" name="file[file]" required="required" class="form-control-file">

As you see it's an array of data called file. So you should make sure taht you submit an array to your form, kind of ['file' => $request->files->all()].

All 13 comments

Did you solve?

No, unfortunately I've still got no idea wether this is a configuration problem or actual bug.

It's likely a problem on your side.
Please provide more infos, like: Symfony version, bundle version, validation rules, template code, when ther error is occurring

Closing for missing feedback

I'm having the same problem since symfony update from v3.3.6 to v3.3.13.
@Anyqax How did you solve this?

I didn't solve it, didn't have time to investigate any further and just handled the upload manually.
I was on the latest symfony version at the time, 3.3.9 I believe. I have not tried it before that version.

@jeroendesloovere Same issue here. Any ideas?

I did have the same problem.I found that it was coming from overwritting the template for my case.
My template wasn't generating the ID and the name of the input like the default one.

'compound' => false, will fix your problem

class SomeType extends AbstractType
{

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'compound'   => false,
                'data_class' => SomeEntity::class
            ]
        );
    }

}

it comes from vendor/symfony/symfony/src/Symfony/Component/Form/Form.php :: submit()

            if ($this->config->getCompound()) {
                if (null === $submittedData) {
                    $submittedData = array();
                }

                if (!\is_array($submittedData)) {
                    throw new TransformationFailedException('Compound forms expect an array or NULL on submission.');
                }

In fact the problem that VichFileType::class generate an input that looks like this
<input type="file" id="file_file" name="file[file]" required="required" class="form-control-file">

As you see it's an array of data called file. So you should make sure taht you submit an array to your form, kind of ['file' => $request->files->all()].

'compound' => false, will fix your problem

class SomeType extends AbstractType
{

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'compound'   => false,
                'data_class' => SomeEntity::class
            ]
        );
    }

}

it comes from vendor/symfony/symfony/src/Symfony/Component/Form/Form.php :: submit()

            if ($this->config->getCompound()) {
                if (null === $submittedData) {
                    $submittedData = array();
                }

                if (!\is_array($submittedData)) {
                    throw new TransformationFailedException('Compound forms expect an array or NULL on submission.');
                }

Came across this old thread looking for an answer after I got the Expected argument of type "string or null", "array" given error.
My "string or null" field was compound so the default value was automatically set to an empty array.
The above answer helped me fix the problem, alternatively, if you want to keep your field compound the following works too :

$resolver->setDefaults(
            [
                'empty_data'   => "",
                'data_class' => SomeEntity::class
            ]
        );

In a case if somebody will face this issue: reason for the error message is a fact that VichFileType is actually compound form, not simple one as it embeds Symfony's FileType internally. Therefore actual data structure is one level deeper then it may be expected.

It took me several hours to figure it out, here is final version. It uses data object for handling uploads via API call, but maybe helpful to see how actual data structure looks like and how it may be converted using view and model transformers.

Upload.php - simple data object to store uploaded file and define validation:

namespace App\DataObject;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;

class Upload
{
    /**
     * @Assert\NotBlank(message="app.upload.required")
     * @Assert\File(
     *      maxSize="10m",
     *      maxSizeMessage="app.upload.too_large"
     * )
     */
    public ?File $file = null;
}

UploadForm.php - Form itself. Notice added view and model transformers and stripped block prefix. Block prefix stripping is only required for handling API requests where it is not necessary to have additional prefix

namespace App\Form;

use App\DataObject\Upload;
use App\Form\Transformer\UploadModelTransformer;
use App\Form\Transformer\UploadViewTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichFileType;

class UploadForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('file', VichFileType::class, [
                'required'          => true,
                'allow_file_upload' => true,
            ])
            ->addModelTransformer(new UploadModelTransformer())
            ->addViewTransformer(new UploadViewTransformer())
            ->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) {
                $data = $event->getData();
                if (is_array($data) && ($data['file'] ?? null) instanceof UploadedFile) {
                    $data['file'] = ['file' => $data['file']];
                    $event->setData($data);
                }
            });
    }

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

    public function getBlockPrefix(): string
    {
        return '';
    }
}

UploadModelTransformer.php - Model transformer for the form. Please notice that instead of having it as ['file'=>$file] it is actually ['file'=>['file'=>$file]]. Lacking of this additional nesting level causes error mentioned into issue's subject.

namespace App\Form\Transformer;

use App\DataObject\Upload;
use Symfony\Component\Form\DataTransformerInterface;

class UploadModelTransformer implements DataTransformerInterface
{
    public function transform($value)
    {
        if ($value instanceof Upload) {
            return ['file' => ['file' => $value]];
        }
        return $value;
    }

    public function reverseTransform($value)
    {
        if (is_array($value) && ($value['file']['file'] ?? null) instanceof Upload) {
            return $value['file']['file'];
        }
        return $value;
    }
}

UploadViewTransformer.php - View transformer for the form. It is exactly opposite version of UploadModelTransformer, but lack of this transformer results into different exception to be thrown by Symfony Form (don't rememeber exact message, it was something about invalid type of submitted data).

namespace App\Form\Transformer;

use App\DataObject\Upload;
use Symfony\Component\Form\DataTransformerInterface;

class UploadViewTransformer implements DataTransformerInterface
{
    public function transform($value)
    {
        if (is_array($value) && ($value['file']['file'] ?? null) instanceof Upload) {
            return $value['file']['file'];
        }
        return $value;
    }

    public function reverseTransform($value)
    {
        if ($value instanceof Upload) {
            return ['file' => ['file' => $value]];
        }
        return $value;
    }
}

@garak Hope this information will help somehow. From my point of view it may be worth mentioning into documentation that form types provided by bundle are actually compound since it is not what is immediately obvious unless you're digging into the code.

@garak Hope this information will help somehow. From my point of view it may be worth mentioning into documentation that form types provided by bundle are actually compound since it is not what is immediately obvious unless you're digging into the code.

Well, it should be evident just if you try to look at the field name, being something like yourform[yourfield][file]. Same if you try to post a form in a functional test.
Anyway, feel free to propose a clarification in documentation with a Pull Request.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

benIT picture benIT  路  3Comments

jcg678 picture jcg678  路  6Comments

vialcollet picture vialcollet  路  5Comments

NicolaPez picture NicolaPez  路  6Comments

webkmua picture webkmua  路  4Comments