File Upload Field in a Form

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We're rocking! We know what it looks like to upload a file in Symfony: we can work with the UploadedFile object and we know how to move the file around. That feels good!

It's time to talk about how to work with a file upload field inside a Symfony form. And then, we also need to save the filename of the uploaded image to our Article entity. Because, ultimately, on the homepage, we need to render each image next to the article.

What your Entity Should Look Like

In the src/Entity directory, let's look at the Article entity. Ok great: the entity is already setup! It has an $imageFilename field that is a string. This is important: the uploaded file will be stored... somewhere: on your server, in the cloud, in your imagination - it doesn't matter. But in the database, the only thing you will store is the string filename.

Adding the FileType to the Form

The form that handles this page lives at src/Form/ArticleFormType.php. In ArticleAdminController... if you scroll up a little bit... here is the edit() action and you can see it using this ArticleFormType. Right now, this is a nice traditional form: it handles the request and saves the Article to the database. Beautifully... boring!

In ArticleFormType, add a new field with ->add() and call it imageFilename because that's the name of the property inside Article. For the type, use FileType::class.

... lines 1 - 11
use Symfony\Component\Form\Extension\Core\Type\FileType;
... lines 13 - 19
class ArticleFormType extends AbstractType
{
... lines 22 - 28
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 31 - 34
$builder
... lines 36 - 53
->add('imageFilename', FileType::class)
;
... lines 56 - 88
}
... lines 90 - 152
}

But... there's a problem with this. And if you already see it, extra credit points for you! Move over and refresh. Woh.

The form's view data is expected to be an instance of class File but it is a string.

Um... ok. The problem is not super obvious... but it clearly hates something about our new field. Here's the explanation: we know that when you upload a file, Symfony gives you an UploadedFile object, not a string. But, the imageFilename field here on Article... that is a string! Connecting the form field directly to the string property doesn't make sense. We're missing a layer in the middle: something that can work with the UploadedFile object, move the file, and then set the new filename onto the property.

Using an Unmapped Field

How can we do that? Change the field name to just imageFile. There is no property on our entity with this name... so this, on its own, will not work. Pretty commonly, you'll see people create this property on their entity, just to make the form work. They don't persist this property to the database with Doctrine... so the idea works, but I don't love it.

Instead, we'll use a trick that we talked a lot about in our forms tutorial: add an option to the field: 'mapped' => false.

... lines 1 - 19
class ArticleFormType extends AbstractType
{
... lines 22 - 28
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 31 - 34
$builder
... lines 36 - 53
->add('imageFile', FileType::class, [
'mapped' => false
])
;
... lines 58 - 90
}
... lines 92 - 154
}

If you've never seen this before, we'll explain it in a minute. Now that we have a new imageFile field, let's go render it! Open edit.html.twig. Remove the HTML form - we're done with that. The Symfony form lives in _form.html.twig. After the title, add {{ form_row(articleForm.imageFile }}.

{{ form_start(articleForm) }}
{{ form_row(articleForm.title, {
label: 'Article title'
}) }}
{{ form_row(articleForm.imageFile) }}
... lines 6 - 23
{{ form_end(articleForm) }}

Nothing special here.

This submits back to ArticleAdminController::edit(). Go inside the $form->isValid() block. When you have an unmapped field, the data will not be put onto your Article object. So, how can we get it? dd($form['imageFile']->getData()).

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
dd($form['imageFile']->getData());
... lines 58 - 66
}
... lines 68 - 71
}
... lines 73 - 128
}

Let's try that! Go back to your browser and hit enter on the URL: we need the form to totally re-render. Hey! There's our new field! Select the astronaut again. Um... did that work? Cause... I don't see the filename on my field. Yes: it did work - we don't see anything because of a display bug if you're using Symfony's Bootstrap 4 form theme. We'll talk about that later. But, the file is attached to the field. Hit Update!

Yes! It's our beloved UploadedFile object! We totally know how to work with that! Oh, but before we do: I want to point out something cool. Inspect element and find the form tag. Hey! It has the enctype="multipart/form-data" attribute! We get that for free because we use the {{ form_start() }} function to render the <form> tag. As soon as there is even one file upload field in the form, Symfony adds this attribute for you. High-five team!

Moving the Uploaded File

Time to finish this. Let's upload a different file - earth.jpeg. And... there's the dump. We have two jobs in our controller: move this file to the final location and store the new filename on the $imageFilename property. Back in the controller, scroll down to temporaryUploadAction(), steal all its code, and delete it.

Up in edit(), remove the dd() and set this to an $uploadedFile variable. Add the same inline phpdoc as last time

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form['imageFile']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads';
... lines 60 - 77
}
... lines 79 - 82
}
... lines 84 - 121
}

then paste the code. Yep! We'll move the file to public/uploads and give it a unique filename. Take off the dd() around move().

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form['imageFile']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads';
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = Urlizer::urlize($originalFilename).'-'.uniqid().'.'.$uploadedFile->guessExtension();
$uploadedFile->move(
$destination,
$newFilename
);
... lines 68 - 77
}
... lines 79 - 82
}
... lines 84 - 121
}

Now, call $article->setImageFilename($newFilename)

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form['imageFile']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads';
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = Urlizer::urlize($originalFilename).'-'.uniqid().'.'.$uploadedFile->guessExtension();
$uploadedFile->move(
$destination,
$newFilename
);
$article->setImageFilename($newFilename);
... lines 69 - 77
}
... lines 79 - 82
}
... lines 84 - 121
}

and let Doctrine save the entity, just like it already was.

Beautiful! I do want to point out that the $newFilename string that we're storing in the database is just the filename: it doesn't contain the directory or the word uploads: it's... the filename. Oh, for my personal sanity, let's upload things into an article_image sub-directory: that'll be cleaner when we start uploading multiple types of things. Remove the old files.

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form['imageFile']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads/article_image';
... lines 60 - 77
}
... lines 79 - 82
}
... lines 84 - 121
}

Moment of truth! Find your browser, roll up your sleeves, and refresh! Um... it probably worked? In the uploads/ directory... yea! There's our Earth file! Let's see what the database looks like - find your terminal and run:

php bin/console doctrine:query:sql 'SELECT * FROM article WHERE id = 1'

Let's see, the id of this article is 1. Yes! the image_filename column is totally set! Fist-pumping time!

Avoid Processing when no Upload

Oh, but there is one tiny thing we need to clean up before moving on. What if we just want to, I don't know, edit the article's title, but we don't need to change the image. No problem - hit Update! Oh... That's HTML5 validation. You might remember from the forms tutorial that this required attribute is added to every field... unless you're using form field type guessing. It's annoying - fix it by adding 'required' => false.

... lines 1 - 19
class ArticleFormType extends AbstractType
{
... lines 22 - 28
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 31 - 34
$builder
... lines 36 - 53
->add('imageFile', FileType::class, [
... line 55
'required' => false,
])
;
... lines 59 - 91
}
... lines 93 - 155
}

Let's try it again. Refresh, change the title, submit and... oof.

Call to a member function getClientOriginalName on null

Of course! We're not uploading a file! So the $uploadedFile variable is null! That's ok! If the user didn't upload a file, we don't need to do any of this logic. In other words, if ($uploadedFile), then do all of that. Otherwise, skip it!

... lines 1 - 16
class ArticleAdminController extends BaseController
{
... lines 19 - 48
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 51 - 55
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form['imageFile']->getData();
if ($uploadedFile) {
$destination = $this->getParameter('kernel.project_dir').'/public/uploads/article_image';
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = Urlizer::urlize($originalFilename).'-'.uniqid().'.'.$uploadedFile->guessExtension();
$uploadedFile->move(
$destination,
$newFilename
);
$article->setImageFilename($newFilename);
}
... lines 71 - 79
}
... lines 81 - 84
}
... lines 86 - 123
}

Refresh now. Got it!

Next: This is looking good! Except that... we need this exact same logic in the new() action. To make a truly killer upload system, we need to refactor the upload logic into a reusable service.

Leave a comment!

  • 2020-01-03 Victor Bocharsky

    Hey Allan,

    Have you watched this tutorial to the end? :) We're talking about multiple files uploading further in https://symfonycasts.com/sc... . Also, we explain why multiple files uploading has bad UI e.g. if one of selected files does not pass validation - none files are saved. So, if you have not watched this course to the end - I'd recommend you to do this first, and I bet you will get answers to your questions :)

    I hope this helps!

    Cheers!

  • 2020-01-02 Allan Ouma

    How can I use this to upload several images in the same form? I am using Symfony 5.

  • 2019-12-19 Victor Bocharsky

    Hey Cool,

    Ah, cool! :) Glad you was able to fix it! And thank you for sharing your solution.

    Cheers!

  • 2019-12-18 Cool life

    Hello Victor Bocharsky ,
    Thanks a lot for your help, I fixed the problem, I've just updated the bootstrap stylesheet in base.html.twig to the latest version 4.4.1 and the issue is fixed, it is as simple as that. :D

  • 2019-12-12 Victor Bocharsky

    Hey Cool,

    Ah, I see... this really sounds like your project is missing some styles, actually that "Browse" button is added with CSS styles. It's difficult to say what exactly you're missing, I'd recommend you to download our course code and compare your HTML structure with the one we have in this screencast. Also keep an eye on styles you're including. Except of possible missing HTML/CSS in your project I find it difficult to say what exactly may cause this problem.

    I hope this helps!

    Cheers!

  • 2019-12-11 Cool life

    Hello Victor Bocharsky, I did desactivate all the extensions, my browser is in his latest version and I do even open the app in Chrome Incognito mode, but the problem is still remaining.

    My PC's OS is Windows 8.1.

    In fact, i did not download this couse code, I've started the project from Zero, step by step, from the first chapiter "Stellar Development with Symfony 4" to the Forms chapiter and continue with this course.

    when I debug with Chrome developer toolbar, it sounds that this fiels has a style of "opacity:0", but when I make the "opacity:1", it gives me the standard file input not the bootsraped one.

    This is the first issue I faced, and it sounds weird.

    This is my Github project's link: https://github.com/mohamedT...

    Thanks a lot for your help and I'm looking forward to your response again :)

  • 2019-12-10 Victor Bocharsky

    Hey Cool,

    Hm, that's weird, I just double-checked with downloaded course code and I do have file path and even a "Browse" button to choose a file - I don't see the button on your screenshot as well. What is your OS? Windows? I'd recommend you to check your browser version, probably there might be some updates and you just need to install and reload your browser first. Also, make sure you don't have any extensions (most probably some add blockers etc.) that might block or modify your page content. Also, try to open this page in Chrome Incognito mode where all plugins are turned off by default.

    If you still does not see it - most probably you have some style issues, it would be good to double check styles and HTML markup, you can debug things in Chrome with Chrome developer toolbar. Btw, did you download the course code for this tutorial and start from the start/ directory? Or did you start a completely separate fresh project? If the second, please, try to download the course code and bootstrap the project from the final/ directory. Did you see the file path and button there?

    Anyway, it sounds more like browser problem, especially if it works perfectly but you can't see the path - that's not something you can tweak with styles or HTML code, it just works out of the box in browsers. So I mostly lean toward that some browser extension modifies the page behind the scene :/

    I hope this helps!

    Cheers!

  • 2019-12-10 Cool life

    Thank you Victor Bocharsky for your response,
    Well, the input file that rendered by the FileType in ArticleFormType does not appear. Although, it perfectly works..
    I use Symfony4.3 and Google chrome as defaut browser. I even tried it in FireFox browser and the same problem remains.
    I'm looking forward to your response.
    Thanks.
    The link of the problem's screenshot : https://i.imgur.com/fqIdXJS...

  • 2019-12-09 Victor Bocharsky

    Hey Cool,

    What do you mean about "the input file doesn't show"? What browser do you use? Could you provide a screenshot maybe? It would help to understand the problem

    Cheers!

  • 2019-12-05 Cool life

    Hello, I have an issue with rendering the input file..Well the input file doesn't show, just the label appears.What is the problem?