Search code examples
symfonysymfony-4.4

Symfony 4.4: entity/model property reserved for the image file upload is newer being set by the form system (result is always null - no errors)


I'm trying to implement an input data filter for the API service based on Symfony 4.4 by using internal form system.

In most cases, it works just fine - integer or text-based fields. Somehow it does not work as expected when it comes to file/image fields. I've tried various integration options from official documentation with no luck.

Due to the legacy code and inconsistency between provided upload field name and the exact entity I prepared a model instead of using the model of the entity where the data of the uploaded file will be actually stored afterwards:

<?php

namespace App\Model;

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

class Avatar {
    /**
     * @var File
     *
     * @Asserts\Image()
     * #Asserts\NotBlank() // Temporary disabled because this property never gets set due to the unknown reason.
     */
    protected $file = null;

    public function setFile(?File $file = null): self
    {
        $this->file = $file;

        return $this;
    }

    public function getFile(): ?File
    {
        return $this->file;
    }
}

Form type looks like this:

<?php

namespace App\Form;

use App\Model\Avatar;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Validator\Constraints;

class AvatarType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', Type\FileType::class, [
                'label' => 'Image',
                'required' => true,
                'mapped' => true,
                'constraints' => [
                    new Constraints\Image([
                        'maxSize' => '5M'
                    ])
                ]
            ])
            ;
    }

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

And finally the controller part:

<?php

namespace App\Controller\Api;

use App\Controller\Api\BaseController;
use App\Entity\User;
use App\Model\Avatar;
use App\Form\AvatarType;
use App\Repository\UserRepository;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/me/avatar", name="app_api.me.avatar", methods={"POST"})
 */
class AvatarController extends BaseController
{
    public function uploadAvatar(User $user, Request $request)
    {
        $avatar = new Avatar();
        $form = $this->createForm(AvatarType::class, $avatar);
        $form->submit($request->request->all());
        if ($form->isSubmitted() && (!$form->isValid())) {
            throw new \Exception((string) $form->getErrors(true));
        }

        dd($avatar->getFile());

        ...
    }
}

When I try to make a POST request to this endpoint using PostMan with the body -> form-data -> file property set find some image file selected I always get null as a result of $avatar->getFile() in the controller.

The result is similar if I use dd($form->getData()); instead of dd($avatar->getFile());

AvatarController.php on line 29:
App\Model\Avatar {#213795
  #file: null
}

I've tried with FormType field property 'mapped' => false and the following way to get data as well but the result is the same - property 'file' never gets set and there is no error reported. It works for all other field types (that I've tested) except FileType.

dd($form['file']->getData()); // results in null

If I add additional fields with other types such as TextType they work as expected:

AvatarController.php on line 29:
App\Model\Avatar {#213795
  #file: null
  #test: "some input text"
}

If I use direct data from the input request it works for the file property but it is unsafe and without any constraints provided by the Symfony functionality.

/** @var UploadedFile $ufile */
$ufile = $request->files->get('file');
dd($ufile);

=>

AvatarController.php on line 34:
Symfony\Component\HttpFoundation\File\UploadedFile {#528
  -test: false
  -originalName: "67922301_10219819530703883_7215519506519556096_n.jpg"
  -mimeType: "image/jpeg"
  -error: 0
  path: "/tmp"
  filename: "phpFHPPNL"
  basename: "phpFHPPNL"
  pathname: "/tmp/phpFHPPNL"
  extension: ""
  realPath: "/tmp/phpFHPPNL"
  aTime: 2020-05-21 17:02:49
  mTime: 2020-05-21 17:02:49
  cTime: 2020-05-21 17:02:49
  inode: 1451769
  size: 145608
  perms: 0100600
  owner: 1000
  group: 1000
  type: "file"
  writable: true
  readable: true
  executable: false
  file: true
  dir: false
  link: false
}

What am I doing wrong here? Any ideas?


Solution

  • The problem is in $form->submit($request->request->all()); line. $request->request is an equivalent of $_POST, files which are in PHP available in the $_FILES superglobal are available through $request->files. Anyway, the best way to avoid such issues is to call $form->handleRequest($request); instead of submitting data manually.