0

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?

Boštjan Biber
  • 444
  • 3
  • 12

1 Answers1

1

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.

malarzm
  • 2,831
  • 2
  • 15
  • 25
  • Thank you for your answer. You pointed me in the right direction. You are right about the $request but the handleRequest method is a bit more tricky. It does not submit the form if the method set in form type does not match (sometimes it is convenient to use same form type for multiple methods e.g. POST, PUT, PATCH to avoid excess code) and it checks if there is a form name included in provided data - this is totally not useful for the API endpoints. – Boštjan Biber May 22 '20 at 00:26
  • I got it working by merging POST and FILES parameters like $form->submit(array_merge($request->request->all(), $request->files->all())); but I don't like this solution. Will try to figure out something else tomorrow. – Boštjan Biber May 22 '20 at 00:26