Search code examples
phpformssymfonysymfony-2.4

File Upload doesn't create UploadedFile


I am currently working on a PHP v5.5.9 project in which I am using some standalone components (I do not use the full-stack framework), currently the following:

psr/log                      1.0.0   Common interface for logging libraries
sonata-project/intl-bundle   2.2.1   Symfony SonataIntlBundle
symfony/debug                v2.4.2  Symfony Debug Component
symfony/dependency-injection v2.4.2  Symfony DependencyInjection Component
symfony/event-dispatcher     v2.4.2  Symfony EventDispatcher Component
symfony/form                 v2.4.2  Symfony Form Component
symfony/http-foundation      v2.4.2  Symfony HttpFoundation Component
symfony/http-kernel          v2.4.2  Symfony HttpKernel Component
symfony/icu                  v1.2.0  Contains an excerpt of the ICU data and classes to load it.
symfony/intl                 v2.4.2  A PHP replacement layer for the C intl extension that includes additional data from the ICU library.
symfony/locale               v2.4.2  Symfony Locale Component
symfony/options-resolver     v2.4.2  Symfony OptionsResolver Component
symfony/property-access      v2.4.2  Symfony PropertyAccess Component
symfony/security-core        v2.4.2  Symfony Security Component - Core Library
symfony/security-csrf        v2.4.2  Symfony Security Component - CSRF Library
symfony/templating           v2.4.2  Symfony Templating Component
symfony/translation          v2.4.2  Symfony Translation Component
symfony/twig-bridge          v2.4.2  Symfony Twig Bridge
symfony/validator            v2.4.2  Symfony Validator Component
twig/extensions              v1.0.1  Common additional features for Twig that do not directly belong in core
twig/twig                    v1.15.1 Twig, the flexible, fast, and secure template language for PHP

I have one Entity class, one FormType class and one Controller and I am able to persist the Entity via a "Twig" template View. So the full stack is working correctly.

Then I added a File attribute to my Entity, in order to add an image file as described in the official documentation]1. I've also extended my FormType with the following code:

$builder->add('pictureFile', 'file', array('label' => 'Image File'));

After running the updated code (with an image file selected), the following error is shown:

Catchable fatal error: Argument 1 passed to MyEntity::setPictureFile() must be an instance of Symfony\Component\HttpFoundation\File\File, array given, called in [.]\vendor\symfony\property-access\Symfony\Component\PropertyAccess\PropertyAccessor.php on line [.] and defined in MyEntity.php on line [.]

So, Symfony does not automatically create a UploadedFile from the form, but binds the $_FILE data to an array. I've noticed, that other people do also have this problem:

At the moment I am using the following code in my Controller, but I would like to use the auto-binding feature (because at the moment I cannot validate the uploaded file in the Entity).

$formType = new MyType;
$entity = new MyEntity;
$form = $this->formFactory->create($formType, $entity);
$form->handleRequest();

if (true === $form->isSubmitted()) {
    if (true === $form->isValid()) {
        // TODO Replace ugly workaround to partially solve the problem of the file upload.
        $request = Request::createFromGlobals();
        $file = $request->files->get('myentity')['pictureFile'];

        // Persist entity, upload file to server and redirect.
    }
}

// Load View template.

Edit (2014-03-03): Here's the relevant code of the Entity:

<?php
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;

class MyEntity
{
    private $id;
    private $name;
    private $picture;

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

    public function setId($id)
    {
        $this->id = $id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getPictureFile()
    {
        return $this->pictureFile;
    }

    public function setPictureFile(File $pictureFile = null)
    {
        $this->pictureFile = $pictureFile;
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint(
            'name',
            new Assert\NotBlank
        );
        $metadata->addPropertyConstraint(
            'name',
            new Assert\Type('string'));
        $metadata->addPropertyConstraint(
            'pictureFile',
            new Assert\Image(
                array(
                    'maxSize' => '2048k',
                    'minWidth' => 640,
                    'maxWidth' => 4096,
                    'minHeight' => 480,
                    'maxHeight' => 4096
                )
            )
      );
    }
}

And the source code of the FormType:

<?php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class MyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name', 'text')
            ->add(
                'pictureFile',
                'file',
                array('label' => 'Image File')
            )
            ->add('submit', 'submit', array('label' => 'Save'));
    }

    public function getName()
    {
        return 'myentity';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array('data_class' => __NAMESPACE__ . '\MyEntity'));
    }
}

Any suggestions how-to get this work?

Edit (2014-03-07): Simplified source code in preparation for my answer.


Solution

  • I've found a solution for the problem described in the question. Since this seems to be a problem many people have, I describe my solution in this answer.

    During my research I've come across a issue on GitHub, which contains a dummy DataTransformer.

    After reading the content of that page, I came up with the idea to try it with a custom DataTransformer which converts the array into an instance of UploadedFile.

    Here is the step-by-step solution:

    1. Create a DataTransformer named UploadedFileTransformer with the following source code:

      use Symfony\Component\Form\DataTransformerInterface;
      use Symfony\Component\Form\Exception\TransformationFailedException;
      use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
      use Symfony\Component\HttpFoundation\File\UploadedFile;
      
      
      class UploadedFileTransformer implements DataTransformerInterface
      {
          /**
           * {@inheritdoc}
           *
           * @param array $data The array to transform to an uploaded file.
           *
           * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null The
           *         uploaded file or `null` if no file has been uploaded.
           */
          public function reverseTransform($data)
          {
              if (!$data) {
                  return null;
              }
      
              $path = $data['tmp_name'];
              $pathinfo = pathinfo($path);
              $basename = $pathinfo['basename'];
      
              try {
                  $uploadedFile = new UploadedFile(
                      $path,
                      $basename,
                      $data['type'],
                      $data['size'],
                      $data['error']
                  );
              } catch (FileNotFoundException $ex) {
                  throw new TransformationFailedException($ex->getMessage());
              }
      
              return $uploadedFile;
          }
      
          /**
           * {@inheritdoc}
           *
           * @param \Symfony\Component\HttpFoundation\File\UploadedFile|null $file The
           *        uploaded file to transform to an `array`.
           *
           * @return \Symfony\Component\HttpFoundation\File\UploadedFile|null The
           *         argument `$file`.
           */
          public function transform($file)
          {
              return $file;
          }
      }
      
    2. Update the FormType to use the UploadedFileTransformer for a file type input field (compare with the code in the question):

      use Symfony\Component\Form\AbstractType;
      use Symfony\Component\Form\FormBuilderInterface;
      use Symfony\Component\OptionsResolver\OptionsResolverInterface;
      
      class MyType extends AbstractType
      {
          public function buildForm(FormBuilderInterface $builder, array $options)
          {
              $builder->add('name', 'text')
                  ->add(
                      $builder->create(
                          'pictureFile',
                          'file',
                          array('label' => 'Image File')
                      )->addModelTransformer(new \UploadedFileTransformer)
                  )
                  ->add('submit', 'submit', array('label' => 'Save'));
          }
      
          public function getName()
          {
              return 'myentity';
          }
      
          public function setDefaultOptions(OptionsResolverInterface $resolver)
          {
              $resolver->setDefaults(array('data_class' => __NAMESPACE__ . '\MyEntity'));
          }
      }
      
    3. Remove crap from the Controller, in my example the access to the Request object to obtain the uploaded file (compare with the code in the question).

      $formType = new MyType;
      $entity = new MyEntity;
      $form = $this->formFactory->create($formType, $entity);
      $form->handleRequest();
      
      if (true === $form->isSubmitted()) {
          if (true === $form->isValid()) {
              $file = $entity->getPictureFile();
      
              // Persist entity, upload file to server and redirect.
          }
      }
      
      // Load View template.
      

    And that's it. No fiddling with attributes in the FormType and the validation in the Entity is also working.

    I really don't know, why this isn't the default behavior of the Symfony components? Maybe I missed to register something in my startup code?