Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

File Validation

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I've ignored it long enough - sorry! We've gotta add some validation to the upload field. Because... right now, we can upload any file type - it's madness! This is supposed to be an image field people! We need to only allow pngs, jpegs, gifs, image stuff.

Validating an Unmapped Field

Normally we add validation to the entity class: we would go into the Article class, find the property, and add some annotations. But... the field we want to validate is an unmapped form field - there is no imageFile property in Article.

No worries: for unmapped fields, you can add validation directly to the form with the constraints option. And when it comes to file uploads, there are two really important constraints: one called File and an even stronger one called Image. Add new Image() - the one from the Validator\Constraints.

... lines 1 - 18
use Symfony\Component\Validator\Constraints\Image;
class ArticleFormType extends AbstractType
{
... lines 23 - 29
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 32 - 35
$builder
... lines 37 - 54
->add('imageFile', FileType::class, [
... lines 56 - 57
'constraints' => [
new Image()
]
])
;
... lines 63 - 95
}
... lines 97 - 159
}

The Image Constraint

And... that's all we need! That's enough to make sure the user uploads an image. Check it out: find your browser, Google for "Symfony image constraint" and click into the docs.

The Image constraint extends the File constraint - so both basically have the same behavior: you can define a maxSize or configure different mimeTypes. The Image constraint just adds... more super-powers. First, it pre-configures the mimeType option to only allow images. And you get a crazy-amount of other image stuff - like minWidth, maxWidth or allowPortrait.

So let's test it! Refresh the page and browse. Oh, the Symfony Best Practices PDF snuck into my directory. Select that, update and... boom! This file is not a valid image.

Validating the File Size

Go back to the docs and click to see the File constraint. The other most common option is maxSize. To see what that looks like, set it to something tiny, like 5k.

... lines 1 - 20
class ArticleFormType extends AbstractType
{
... lines 23 - 29
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 32 - 35
$builder
... lines 37 - 54
->add('imageFile', FileType::class, [
... lines 56 - 57
'constraints' => [
new Image([
'maxSize' => '5k'
])
]
])
;
... lines 65 - 97
}
... lines 99 - 161
}

Ok: browse and select any of the files. Hit update and... perfect: the file is too large.

Change that back to 5M, or whatever makes sense for you.

... lines 1 - 20
class ArticleFormType extends AbstractType
{
... lines 23 - 29
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 32 - 35
$builder
... lines 37 - 54
->add('imageFile', FileType::class, [
... lines 56 - 57
'constraints' => [
new Image([
'maxSize' => '5M'
])
]
])
;
... lines 65 - 97
}
... lines 99 - 161
}

Validation and upload_max_filesize

Oh, but, remember a few minutes ago when we tried to upload the stars photo? It's 3 megabytes, which is way under the 5 megabytes we just set, but above my php.ini upload_max_filesize setting. That caused a really nasty error.

Well, try selecting it again and updating. Yes! When you use the File or Image constraint, they also catch any PHP-level upload errors and display them quite nicely. You can customize this message.

Making the Upload Field Required

And... that's it! Sure, there are a more options and you can control all the messages - but that's easy enough. Except... there is one tricky thing: how can we make the upload field required? Like, when someone creates an article, they should be required to upload an image before saving it.

Simple, right? Just add a new NotNull() constraint to the imageFile field. Wait, no, that won't work. If we did that, we would need to upload a file even if we were just editing a field on the article: we would literally need to upload an image every time we changed anything.

Okay: so we want the imageFile to be required... but only if the Article doesn't already have an imageFilename. Start by breaking this onto multiple lines. Then say $imageConstraints =, copy the new Image() stuff and paste it here.

... lines 1 - 20
class ArticleFormType extends AbstractType
{
... lines 23 - 29
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 32 - 35
$builder
... lines 37 - 45
->add('location', ChoiceType::class, [
... lines 47 - 53
])
;
... line 56
$imageConstraints = [
new Image([
'maxSize' => '5M'
])
];
$builder
->add('imageFile', FileType::class, [
... lines 64 - 66
])
;
... lines 69 - 101
}
... lines 103 - 165
}

Down below, set 'constraints' => $imageConstraints. Oh... and let's spell that correctly.

... lines 1 - 20
class ArticleFormType extends AbstractType
{
... lines 23 - 29
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 32 - 61
$builder
->add('imageFile', FileType::class, [
... lines 64 - 65
'constraints' => $imageConstraints
])
;
... lines 69 - 101
}
... lines 103 - 165
}

Now we can conditionally add the NotNull() constraint exactly when we need it. Scroll up a little. In our forms tutorial, we used the data option to get the Article object that this form is bound to. If this is a "new" form, there may or may not be an Article object - so this will be an Article object or null. I also used that to create an $isEdit variable to figure out if we're on the edit screen or not.

We can leverage that by saying if this is not the edit page or if the article doesn't have an image filename, then take $imageConstraints and add new NotNull(). We'll even get fancy and customize the message: Please upload an image.

... lines 1 - 19
use Symfony\Component\Validator\Constraints\NotNull;
class ArticleFormType extends AbstractType
{
... lines 24 - 57
$imageConstraints = [
new Image([
'maxSize' => '5M'
])
];
if (!$isEdit || !$article->getImageFilename()) {
$imageConstraints[] = new NotNull([
'message' => 'Please upload an image',
]);
}
... lines 69 - 109
}
... lines 111 - 173
}

Just saying if !$isEdit is probably enough... but just in case, I'm checking to see if, somehow, we're on the edit page, but the imageFilename is missing, let's require it.

Cool: testing time! Refresh the entire form, but don't select an upload: we know that this Article does have an image already attached. Hit update and... works fine! Now try creating a new Article, fill in a few of the required fields, hit create and... boom! Please upload an image!

Validation, check! Next, let's fix how this renders: we've gotta see the filename after selecting a file - seeing nothing is bummin' me out.

Leave a comment!

34
Login or Register to join the conversation

Even the webserver settings have to be tweaked to allow large image sizes. On Nginx the client_max_body_size sgould be set in the server block.

1 Reply

Hey Sridhar,

Yeah, usually webserver/PHP defaults are low, so you might want to tweak the config to allow large image sizes, thanks for this tip!

Cheers!

Reply
Vincent R. Avatar
Vincent R. Avatar Vincent R. | posted 10 months ago

Hi !

I've made a form which is quite similar to the tutorial's one.
But when I want to edit an (Article-like) object which already has an image, I get a not-found error ('The file could not be found.').
I notice that inside the FileValidator, the $value variable is a string that contains only the raw file name (without any folder name).
I guess it don't find the file because it doesn't know where to search.
So my question is : why does it work in your tuto ???

Reply

Hey Vincent R.

You can find answer inside chapter 9 of this tutorial ;)

Cheers!

Reply
Vincent R. Avatar

Hi Vladimir Sadicov
Thanks for your answer,

I read it again and again (and the chapter 10 as well), I compared my code and the tutorial's one... and I still can't find the bug !
To reach my edit form, I use a button in a page who displays the object details, and in this page I can see the corresponding image, so my getImagePath and getPublicPath functions, as well as my Twig extension (that are the same as the tutorial's) actually work.
The only difference is that my constant is not in the UploaderHelper class but in the object entity class... but I think it can't be the bug.

In the profiler, I have this error :
The file could not be found. car Caused by:

Symfony\Component\Validator\ConstraintViolation {#12787 ▼
root: Symfony\Component\Form\Form {#13462 …}
path: "data.imageFilename"
value: "1136_img_9570-61e14ffeb166c.jpg"
}

So the form seems not to detect the folder where my image is, whereas twig finds it.

Cheers !

Reply

Oh I think I got it. You are trying to save object with already uploaded file, the key moment here that you should skip file validation and changing if you haven't upload new file, that's why you got an error. You should have conditional constraints inside your FormType. If you have course code, check finish folder file

src/Form/ArticleFormType.php

Cheers

Reply
Vincent R. Avatar

The more I'm searching, the more I think that the point is not validation : I get the same error if I try to upload a new file.
In my FormType, I've tried to force it to recognize the former image file by adding :

if ($car->getImagePath()) {
$imagePath = $this->uploader->getPublicPath($car->getImagePath());
$fileTypeOptions['data'] = new UploadedFile($imagePath, $car->getImageFilename(), mime_content_type($imagePath));
}

There $imagePath is exactly the path I successfully use to display the image outside of the form :

/uploads/car_image/1136_img_9570-61e14ffeb166c.jpg

But then I get a new error :

mime_content_type(/uploads/car_image/1136_img_9570-61e14ffeb166c.jpg): failed to open stream: No such file or directory


It doesn't make any sense for me : why does the same path work to display an image out of the form and not inside of it ?

Reply
Vincent R. Avatar
Vincent R. Avatar Vincent R. | Vincent R. | posted 10 months ago | edited

Vincent R.

OK I fixed it.
The solution was in the symfony doc :

When creating a form to edit an already persisted item, the file form type still expects a File instance. As the persisted entity now contains only the relative file path, you first have to concatenate the configured upload path with the stored filename and create a new File class:

I think it could be useful to add this tip to the tutorial.

Thanks for having brainstormed with me !!!

Reply

Lets say I use a JS framework like AngularJS for the UI should I check for specific filetypes when the file is chosen (by checking the file extension) or is there a way Symfony can check the file type using guess extension and return a message once the file is uploaded?

Reply

Hey Sridhar,

You can specify what mime types are allowed with the Image constraint, look at: https://symfony.com/doc/cur... - i.e. by default all image types are allowed, but you can constraint to jpeg images only.

Cheers!

1 Reply
Dimitris T. Avatar
Dimitris T. Avatar Dimitris T. | posted 2 years ago

Hi,

I am using a similar approach in my controller

$violations = $validator->validate($uploadedFile, [
new File(['maxSize' => '10M']),
]);
if ($violations->count() > 0) {
return $this->json(['success' => false, 'violations' => $violations], Response::HTTP_NOT_ACCEPTABLE);
}

I upload a 15M file, the violation occurs, the response has the correct code however the body is empty.

If I set 'maxSize' => '1M' instead, the response body is properly set. My upload_max_filesize is 40M and post_max_size is 40M and the server I am using is apache.

Does this make sense? Any ideas?

Reply

Hey Dimitris T.

I see you are using NOT_ACCEPTABLE http response code, IIRC it's 406, I'm not sure but did you tried to sent 400 code? Probably browser ignores response body on 406 code

Cheers!

Reply
Dimitris T. Avatar
Dimitris T. Avatar Dimitris T. | sadikoff | posted 2 years ago

Yes, I tried 400 as well, unfortunately the problem is independent of the return code ...

Reply

Interesting! Does PHP configured as apache2 module? or you are using fastcgi interface, as an experiment, can you try same action but with symfony cli server? or native php php -S 127.0.0.1:8000 -t public

Cheers!

Reply
Ernest R. Avatar
Ernest R. Avatar Ernest R. | posted 2 years ago

Hello team,
I'm working with symfony 4.4, I have a portofolioPage class that has 4 collections of a class Image in a relationship OneToMany

1)I have in my form a upload field with option multiple and was working fine until I added the constraint for maxsize in my formType and I'm getting an error "This value should be of type string".
this is in my Portofolio entity

/**
* @ORM\OneToMany(targetEntity="App\Entity\Image", mappedBy="natureGallery", cascade={"persist"})
*/
private $natureGallery;

this in my image entity

/**
* @ORM\ManyToOne(targetEntity="App\Entity\PortofolioPage", inversedBy="natureGallery", cascade={"persist"})
*/
private $natureGallery;

and my formType

$imageConstraints = [
new Image(['maxSize' => '5M'])
];
$builder
->add('natureGallery', FileType::class, [
'multiple' => true,
'label' => 'form.natureGallery',
'mapped' => false,
'required' => false,
'constraints' => $imageConstraints,
])

2) My second problem is if the multiple option is removed ( just for checking purposes ) it stills only allow me to upload a max of 2M, in my php.ini i set it to upload_max_filesize = 50M and post_max_size = 50M, so i don't think is really checking my constraints.

Thanks for your time.

Reply
Ernest R. Avatar

Ok, i get rid of the "This value should be of type string".
in my formType:
'constraints' => new All([
new Image([
'maxSize' => '5M'
])
]),

but my validator checks 2 times and stills doesn't allow me to upload files more big than 2M.

Reply

Hey Ernest R.

If you still get error with 2M limit than you should check phpinfo() looks like your change in php.ini has no effect, it can be wrong php.ini file, some systems use different ini files depending on php working mode cli, fpm. The best way to check what you really have is to print phpinfo somewhere or look in the webdebug toolbar

Cheers

Reply
Ernest R. Avatar

Ok I fixed, I had to modify the php.ini from the cli folder, which is the one symfony uses to start the server. Still doesn't allows me to upload a certain M in one push but... that will another day and right now for my project upload 20 images is enough.

Thanks Vladimir.

PD the phpinfo() show the php.ini config in my apache folder.

Reply
Cesar Avatar

Hello. I set file validation maxSize = 5M in my form and I set upload_max_filesize = 5M and post_max_size = 8M in php.ini. However, I have the following issues:
- If the user tries to upload a file bigger then 8M, a php error appears instead of the form error.
- If the user tries to upload a really big file, my app tries to upload it anyway instead of trow the error and then it blows up.
Do you know if I need to do an extra configuration?

Reply

Hey Cesar

> If the user tries to upload a file bigger then 8M, a php error appears instead of the form error.
That's because you set upload_max_filesize to 5M. Increase it to 8 and you should see your error form. Seems like when someone uploads a file bigger than your php/server settings it throws a very generic error at a higher level than the "form validations" layer

> If the user tries to upload a really big file, my app tries to upload it anyway instead of trow the error and then it blows up.
Hmm, what you mean by "blows up" do you get an error? or it just crashes? I think that behavior is specific to your web server. Which web server are you using?

Cheers!

Reply

Hi! I'm passing already something to my form, I'm using a single entity for news which is very similar to another type of article so I have used in the db a flag, so I need to pass a HiddenValue to enable or not some form validation (correct me if there is a better way to do that), the problem is that part of the code:

/** @var Article|null $article */
$article = $options['data_class'];
$type = $options['type'];
$isEdit = $article && $article->getId();

I'm not able to extract $article because now is an array and no more a single object so I get:

Call to a member function getId() on array

How can I solve that?

Reply

Hey Gballocc7

Why you have to pass a hidden value? If you have a specific section for creating your articles, then, on your controller you can set that entity up instead of relying on hidden data on your form. If you are not working with a Entity Form Type, then you can access to the data as a normal array, i.e. $article['id'] but why it's not an entity anymore?

Cheers!

Reply

I'm doing like that because, I got a single Entity but I have slightly different type of article, one has the image and one instead of that holds a pdf. I tried to pass the article with his type already set from the controller to the form but I wasn't able to do that because when I do $article->setType(0) the controller say UNDEFINED VARIABLE.
I need the type in the form because I wanna render some fields only, and because they hold the constraints I can't just render all the fields, because if I try to submit a type 0 article I get that the pdf is missing and viceversa.

For the moment I have done 2 different type of forms is that a bad idea?

Reply

Oh, I get it. Having 2 forms is not that bad, some times here in SfCasts, we create a FormType based on a use case. You can go crazy with Form events so you end up with only one form but I wouldn't recommend it, it's a bit chaotic.

Cheers!

1 Reply

Yeah I solved splitting in different forms for different types of article! Thank you for your advice!

Another issue that I have is with mime, I'm using:

* @Assert\All(@Assert\File(
* maxSize = "5M",
* mimeTypes = {"application/pdf", "application/x-pdf"},
* mimeTypesMessage = "Perfavore inserisci un pdf"
* ))

but this rules aren't working, if I try to upload a >2M file I get:

The file is too large. Allowed maximum size is 2 MiB.

Can't understand why. Also if I put the wrong type I get the message:

This value should be a string.

Why doesn't it use my mimeTypesMessage?

Reply

Ha! I know that error (File size error), it's because you have to configure your php.ini file. There are two settings you need to change:


; Maximum allowed size for uploaded files.
upload_max_filesize = 20M

; Must be greater than or equal to upload_max_filesize
post_max_size = 20M

After that you should be hitting your file constraints

Reply

It doesn't still hit the pdf constraint.. How can I do? I need to upload multiple pdf files but now he makes me upload everykind of file even thought I have put the constraint in the entity

Reply

Ok, but you are not getting the previous error, right? So, finally the form is submitting "correctly". How is composed your FormType? Is it an embeded form? if that's your case, you should read this part of the docs: https://symfony.com/doc/cur...

Reply

In the entity I have:
/**
* @ORM\Column(type="json", nullable=true)
* @Assert\All(@Assert\File(
* maxSize = "5M",
* mimeTypes = {"application/pdf", "application/x-pdf"},
* mimeTypesMessage = "Perfavore inserisci un pdf"
* ))
*/
private $documents = [];

Reply

Why you need @Assert\All? Try removing it

Reply

I have removed it and I get the error:
data.documents This value should be of type string.
[]

Can't understand why, is the problem that is taking from a single form an array of uploaded documents? Here my uploader helper funcion:

public function uploadArticleDocument(array $files, ?array $existingFilename): array
{
$documentsPath = array();
foreach($files as $key=>$file) {
if ($file instanceof UploadedFile) {
$originalFilename = $file->getClientOriginalName();
} else {
$originalFilename = $file->getFilename();
}

$newFilename = Urlizer::urlize(pathinfo($originalFilename, PATHINFO_FILENAME)) . '-' . uniqid() . '.' . $file->guessExtension();

$stream = fopen($file->getPathname(), 'r');
$result = $this->filesystem->writeStream(
self::ARTICLE_DOCUMENT . '/' . $newFilename,
$stream
);

if ($result === false) {
throw new \Exception(sprintf('Could not write uploaded file "%s"', $newFilename));
}
if (is_resource($stream)) {
fclose($stream);
}
if ($existingFilename) {
try {
$result = $this->filesystem->delete(self::ARTICLE_DOCUMENT . '/' . $existingFilename[$key]);
if ($result === false) {
throw new \Exception(sprintf('Could not delete old uploaded file "%s"', $existingFilename[$key]));
}
} catch (FileNotFoundException $e) {
$this->logger->alert(sprintf('Old uploaded file "%s" was missing when trying to delete', $existingFilename[$key]));
}
}

array_push($documentsPath, $newFilename);
}

return $documentsPath;
}

Reply

Ohh, look at the column type you specified, it's "json", it should be an array

1 Reply

Solved switching documents with references table. But this solve only one of my problem, I have the edit like you have done in this course, but for creation? Can I do a similar thing? Does in symfony exist a transaction? I was thinking about doing a commit when I'm creating and putting references and if the form is submit and valid everything get saved. Otherwise if something go wrong or the user exit from the page everything get rollback. Is it possible in symfony?

Reply

Yes, it's possible but it's a feature coming from Doctrine actually. Give it a check to this docs: https://www.doctrine-projec...

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}