Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Upload Fields

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 $12.00

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

Login Subscribe

Our User class also has a property called $avatar:

... lines 1 - 15
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 18 - 50
#[ORM\Column(nullable: true)]
private ?string $avatar;
... lines 53 - 281
}

In the database, this stores a simple filename, like avatar.png. Then, thanks to a getAvatarUrl() method that I created before the tutorial, you can get the full URL to the image, which is /uploads/avatars/the-file-name:

... lines 1 - 15
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 18 - 204
public function getAvatarUrl(): ?string
{
if (!$this->avatar) {
return null;
}
if (strpos($this->avatar, '/') !== false) {
return $this->avatar;
}
return sprintf('/uploads/avatars/%s', $this->avatar);
}
... lines 217 - 281
}

To get this to work, if you create a form that has an upload field, we need to move the uploaded file into this public/uploads/avatars/ directory and then store whatever the filename is onto the avatar property.

Let's add this to our admin area as an "Upload" field and... see if we can get it all working. Fortunately, EasyAdmin makes this pretty easy! It's like it's in the name or something...

The ImageField

Back over in UserCrudController (it doesn't matter where, you can have this in whatever order you want), I'm going to say yield ImageField::new('avatar'):

... lines 1 - 12
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
... lines 14 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 26
yield ImageField::new('avatar');
... lines 28 - 45
}
}

If you have an upload field that is not an image, there isn't a generic FileField or anything like that. But you could use a TextField, then override its form type to be a special FileUploadType that comes from EasyAdmin. Check the ImageField to see what it does internally for more details.

Anyways, let's see what this does. Head back to the user index page and... ah! Broken image tags! But they shouldn't be broken: those image files do exist!

Setting the Base Path

Inspect element on an image. Ah: every image tag literally has just / then the filename. It's missing the /uploads/avatars/ part! To configure that, we need to call ->setBasePath() and pass uploads/avatars so it knows where to look:

... lines 1 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 26
yield ImageField::new('avatar')
->setBasePath('uploads/avatars');
... lines 29 - 46
}
}

If you're storing images on a CDN, you can put the full URL to your CDN right here instead. Basically, put whatever path needs to come right before the actual filename.

Setting the Upload Dir

Head back over, refresh and... got it! Now edit the user and... error!

The "avatar" image field must define the directory where the images are uploaded using the setUploadDir() method.

That's a pretty great error message! According to this, we need to tell the ImageField() that when we upload, we want to store the files in the public/uploads/avatar/ directory. We can do that by saying ->setUploadDir() with public/avatars/uploads:

... lines 1 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 26
yield ImageField::new('avatar')
->setBasePath('uploads/avatars')
->setUploadDir('public/avatars/uploads');
... lines 30 - 47
}
}

Um, actually that path isn't quite right.

And when I refresh... EasyAdmin tells me! The directory actually is public/uploads/avatars. Now that I've fixed that... it works. And that's nice!

The field renders as an upload field, but with a "delete" link, the current filename and even its size! Click the file icon and choose a new image. I'll choose my friend Molly! Hit save and... another error.

You cannot guess the extension as the Mime component is not installed. Try running composer require symfony/mime.

The Mime component helps Symfony look inside of a file to make sure it's really an image... or whatever type of file you're expecting. So, head over to your terminal and run:

composer require symfony/mime

Once that finishes, spin back over, hit refresh to resubmit the form and... yes! There's Molly! She's adorable! And if you look over in our public/uploads/avatars/ directory, there's the file! It has the same filename as it did on my computer.

Tweaking the Uploaded Filename

That's... not actually perfect... because if someone else uploaded an image with the same name - some other fan of Molly - it would replace mine! So let's control how this file is named to avoid any mishaps.

Do that by calling ->setUploadedFileNamePattern(). Before I put anything here, hold Cmd or Ctrl to open that up... because this method has really nice documentation. There are a bunch of wildcards that we can use to get just the filename we want. For example, I'll pass [slug]-[timestamp].[extension], where [slug] is, sort of a cleaned-up version of the original filename:

... lines 1 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 26
yield ImageField::new('avatar')
... lines 28 - 29
->setUploadedFileNamePattern('[slug]-[timestamp].[extension]');
... lines 31 - 48
}
}

By including the time it was uploaded, that will keep things unique!

Ok, edit that same user again, re-upload "Molly", hit "Save" and... beautiful! It still works! And over in the file location... awesome! We now have a "slugified" version of the new file, the timestamp, then .jpg. And notice that the old file is gone! That's another nice feature of EasyAdmin. When we uploaded the new file, it deleted the original since we're not using it anymore. I love that!

Handling Non-Local Files & FileUploadType

Oh, and many people like to upload their files to something like Amazon S3 instead of uploading them locally to the server. Does EasyAdmin support that? Totally! Though, you'll need to hook parts of this up by yourself. Hold Cmd or Ctrl to open ImageField. Behind the scenes, its form type is something called FileUploadType. Hold Cmd or Ctrl again to jump into that.

This is a custom EasyAdmin form type for uploading. Scroll down a bit to find configureOptions(). This declares all of the options that we can pass to this form type. Notice there's a variable called $uploadNew, which is set to a callback and $uploadDelete, which is also set to a callback. Down here, these become the upload_new and upload_delete options: two of the many options that you can see described here.

So if you needed to do something completely custom when a file is uploaded - like moving it to S3 - you could call ->setFormTypeOption() and pass upload_new set to a callback that contains that logic.

So it's very flexible. And if you dig into the source a bit, you'll be able to figure out exactly what you need to do.

Next, it's time to learn about the purpose of the formatted value for each field and how to control it. That will let us render anything we want on the index and detail page for each field.

Leave a comment!

37
Login or Register to join the conversation
Andrew-F Avatar
Andrew-F Avatar Andrew-F | posted 9 months ago | edited | HIGHLIGHTED

For those interested in the easiest way to upload images to s3 using EasyAdmin:
1) Setup Flysystem or Gaufrette All about Uploading Files in Symfony
2) Install VichBundle
3) Configure VichBundle with Flysystem or Gaufrette DOCS
4) Configure your entity getters and setters Preparing your Entities to Persist Images
5) Add some code to your EasyAdmin entity controller

yield Field::new('imageFile')
    ->setFormType(VichImageType::class)
    ->onlyOnForms();
yield ImageField::new('image')
    ->setUploadDir('/') // required by EasyAdmin, you can leave it like that
    ->setBasePath($this->getParameter('your_cdn_url'))
    ->hideOnForm();

Cheers!

1 Reply

Hey Andrew!

Thank you for sharing this tip with others!

Cheers!

1 Reply
Tien dat L. Avatar
Tien dat L. Avatar Tien dat L. | posted 1 year ago | edited

Hi this ImageField now allow upload all type of file, i want to only allow image-type. With Constraints like


            ImageField::new('avatar')
                ->setBasePath('uploads/avatars')
                ->setUploadDir('public/uploads/avatars')
                ->setUploadedFileNamePattern('[slug]-[timestamp].[extension]')
	            ->setFormTypeOptions([
					'constraints' => [
						new Image()
					]
	            ])
                ->onlyOnForms(),

I got others error ' File not found', how can i fix this

1 Reply
Ajie62 Avatar
Ajie62 Avatar Ajie62 | Tien dat L. | posted 1 year ago | edited

I have the exact same error, and I've been stuck on this for two days. 😒 I tried to put the constraint on the entity property, tried to put it in the CrudController, just like Tien dat L. did, but it doesn't work. I keep having this "File could not be found" error. When the constraint is removed, the problem disappears, but of course that's not a solution. As it is, people can upload anything, and it's a problem. Right?

3 Reply
Victor Avatar Victor | SFCASTS | Ajie62 | posted 1 year ago | edited

Hey Jérôme,

Ah, I probably see what you and Ajie62 mean! Yeah, this sounds like a bug in EasyAdmin fairly speaking. Well, first of all we're taking about admin interface, so technically only admins may upload files, so it should not be a big problem that you can upload anything, though I agree that it's not the desired/logical behaviour that you can upload anything. Actually, I think that this ImageField should work out-of-the-box even without the Image constraint make it possible to upload only images. I found a related issue: https://github.com/EasyCorp... - please track its status there, also there're some tips/workarounds. Could you try them? I suppose some of them may work for you.

But behaviour that you described is weird, and I think you can even open an issue in EasyAdmin repo reporting this - you should be able to add that Image constraint to add more custom restrictions.

Cheers!

2 Reply
Ajie62 Avatar

Hi Victor,

Thanks for your quick answer! You're right, the ImageField should work out-of-the-box without the Image constraint. As part of this course, we're indeed in the admin, so I guess people would know what they're uploading, but you know what they say... Never trust user input. If in a website, you allow people to post images for example, this would be a problem.

I created an issue in the EasyAdmin repo: https://github.com/EasyCorp... . Hopefully I was clear enough. This is the first time I do it. 😂 I'll stay on the lookout to see if I get an answer.

Thanks again!

2 Reply
Tien dat L. Avatar
Tien dat L. Avatar Tien dat L. | Ajie62 | posted 1 year ago

Hi Jérôme, thanks for creating an issue =)

Reply
Ajie62 Avatar

I was so upset with this issue... I didn't want to wait. 😂

Reply

Hey Jerome,

Great, thanks for creating an issue! Sounds good to me, if anything is unclear to someone - I suppose people will ask some questions to you for clarifications ;) Let's track its status there.

Cheers!

Reply

Hey Luong,

You do it correctly! I believe the only problem is that this course project does not have "symfony/validator" package installed. Please, first install it with Composer:

$ symfony composer require symfony/validator

Then try again. Now the Image validation constraint should be present :) Also, make sure you use the correct namespace for this class which is "Symfony\Component\Validator\Constraints\Image".

UPD: Ah, I think I know what you mean! See another my comment: https://symfonycasts.com/sc...

Cheers!

1 Reply
Tien dat L. Avatar
Tien dat L. Avatar Tien dat L. | Victor | posted 1 year ago

Hi Victor, thanks for your answer.

Reply
John Avatar
John Avatar John | posted 1 year ago | edited

<spoiler>What a great class!</spoiler>
Hi!
Coming from the course <i>"All about Upload Files in Symfony"</i> (with S3), at minute 6:50 you talk about setting a callback (upload_new) to ->setFormTypeOption()... What exactly logic can I put inside the callback to move that file? that is being uploaded to my S3?... are there more ways to connect EasyAdmin with S3 to upload my documents?

PS: the ->setUploadDir() option is required for this type of fields, but it only interprets the parameters as a local path... What can be done if the directory is an S3 bucket?

Thank you very much for your attention, and congratulations for the quality of teaching, I am fascinated by the courses and the videos. You are incredible!

1 Reply
Mouhammed D. Avatar
Mouhammed D. Avatar Mouhammed D. | John | posted 1 year ago

Hi,
I think, the best way is to use the VichUploaderBundle. It has a seamlessly integration with EasyAdmin. You can therefore configure VichUploaderBundle to upload data to the storage of your choice (flysystem where you have already setup an S3 storage) and use the setFormType method to use VichImageType or VichFileType
Here are some links that may help you.
- https://github.com/dustin10...
- https://github.com/dustin10...
- https://symfony.com/bundles...

Best.

1 Reply

Hi there!

Sorry for the slow reply - some "life" happened! :D. And thank you for the super kind words!

So, yes, this... might be a bit tricky - I haven't tried it myself. First, yes, you need to call setUploadDir() and set this to a LOCAL path... which is weird. But, I'm not convinced that this is needed. What I mean is: I think if you set this to any real path, but set other options correctly, things will work.

Specifically, you would need to set these options:

A) upload_new to a callback with (UploadedFile $file, string $uploadDir, string $fileName) arguments. Inside, you would use a connection to S3 (I like to use Flysystem) to save that UploadedFile contents to S3. $uploadDir will equal what you passed to setUploadDir()... which you can just ignore.

B) upload_delete set to a callback with (File $file) argument. Find that file on S3 and delete it.

C) You may also need to set download_path to a callback with Options $options argument where you return the public path to the file on S3 (but without the filename, that is added later).

I'm still not 100% sure if I'm missing any detail. And after you upload a file, you may not see the usual information about the existing file. That's because EasyAdmin's form type uses a "model transformer" that seems to assume the file is stored locally.

This is pretty fuzzy information - sorry about that - but if you want to try this out, I'd be happy to help debug.

Cheers!

Reply

Hi, thanks for this clues. They work, but not quite....

1). The problem is that I need full url path to s3 file in database not jus file name. How can I achieve this?
What i have in DB: fine_name.png
What i need in DB: https://s3.amazonaws.com/bucket/images/fine_name.png
Note: Upload works, file has been uploaded to the right place on s3.

To solve this i use events (BeforeEntityPersistedEvent|BeforeEntityUpdatedEvent) and just prepend field with s3 url.

2). Other big problem is that when you edit, easyadmin do not see uploaded file (what you mention about "model transformer"), so file input is empty and after sending form there is an error:

Expected argument of type "string", "null" given at property path "image".

I solved it as follows...
On entity: allow field to be null and adjust setter to be something like this (i do not require this field on edit):

public function setImage(?string $image): void
{
    if ($image === null) {
        return;
    }

    $this->image = $image;
}

Partly it solves problem in my case, but unfortunately, when editing, the user has no feedback that the files are uploaded and...

3). On edit, old file is not deleted because $uploadDelete($file); code is never reached because of:

// in AbstractCrudController::processUploadedFiles
$state->hasCurrentFiles() is always false

It looks like EasyAdmin is completely not easy about cloud file upload integration.

Reply

Hey kwolniak!

Thanks for posting your problems and some nice solutions :).

My guess is that issues 2 and 3 are related: EasyAdmin (or probably more its FileUploadType) doesn't "see" the file upload... and so it's missing when you submit, when trying to show the user some feedback (that one if already uploaded) and also when deleting the old file. As you mentioned, the root cause is probably the model transformer used on that field: https://github.com/EasyCorp/EasyAdminBundle/blob/6d5e505b51df4d38b4839594da837e842866c6bc/src/Form/Type/FileUploadType.php#L48

You could, I think, replace that with your own... but it's non-trivial (so I agree that it's not nearly as easy as it should be). I would create a "form type extension" so that you can modify FileUploadType then add your own model transformer... though you may also need to call $builder->resetModelTransformers() to remove the existing one. I'd be interested if someone could get that working... but yea, it's still too complex.

Cheers!

2 Reply
Jeroen-V Avatar
Jeroen-V Avatar Jeroen-V | posted 9 days ago

Whenever I edit a record, my image will be set to null. Is there a solution to fix this? It doesn't seem like someone has fixed it ..

Reply

Hey @Jeroen-V!

Hmmm, that should not be happening! I just tried the code from this step: if I edit something unrelated on the user (e.g. the email) and save, the image is not removed or set to null. I believe this is handled in EasyAdmin's FileUploadType class: in the mapFormsToData() method, it checks to see if the uploaded file from before vs now is "modified" - https://github.com/EasyCorp/EasyAdminBundle/blob/4.x/src/Form/Type/FileUploadType.php#L210 - and doesn't set the avatar property on User at all if it is not. If no file is uploaded, that ->isModified() returns false for me.

So... I'm not sure what's going on in your case. Do you have any other custom code surrounding this?

Cheers!

Reply
Bogdanas Avatar

How to preserve image when editing entity? Now it is set to null every time I edit entity

Reply

Hey Bogdanas,

That's unexpected. Is something in the editing process that's setting that field to null? If that's not the case, I'd removed the image field from the edit action

Hope it helps. Cheers!

Reply

How to block automatical delete action on ImageUpdate. If I want to store previous file as well ?->setFormTypeOption('upload_new', function(???????) {}) How to save it? Can you please share example ?

Reply
Victor Avatar Victor | SFCASTS | Mepcuk | posted 1 year ago | edited

Hey Maxim,

Well, the signature of that callback is the next :


            ->setFormTypeOption('upload_new', function (UploadedFile $uploadedFile, string $uploadDir, string $fileName) {
                // Do something with the just uploaded file... basically, you just need to move it to the upload dir manually.
                // The default code is simple:
                // $uploadedFile->move($uploadDir, $fileName);
            })

And that's it... you can do whatever you want in that callback but don't forget to move that uploaded file otherwise it will be lost.

Buuuut, if you want keep the old file when uploading a new one - you don't need that "upload_new" at all. All you need is to add a "upload_delete" callback:


            ->setFormTypeOption('upload_delete', function (File $file) {
                // Well, technically you don't need to do anything here :) and this will keep the old file in your filesystem
                // The default code in this callback is:
                // unlink($file->getPathname());
                // i.e. it actually deletes the file. So if you will do nothing here - you will not delete that file :)
            })

I hope this was useful for you!

Cheers!

1 Reply

Great answer and explanation. Thanks

Reply

Hey Maxim,

Great! I'm glad it was useful/helpful to you ;)

Chees!

Reply

Hi, how to upload from project-core directory/uploads ? $projectDestination = $this->getParameter('kernel.project_dir');

Reply

Hey Maxim,

You can control upload dir with ImageField::setUploadDir() - specify any folder you want to upload to. But keep in mind that if you will upload images outside of the public/ directory - which is your document root - you won't be able to display them on the website, i.e. images will be outside of your document root, and so EasyAdmin won't be able to render the uploaded image correctly and you may want to tweak the default template to workaround it, i.e. do not render the image for example.

I hope this helps!

Cheers!

Reply

Mmm, if it is public/images how to protect images, i mean that images not possible to see directly, but possible to see via easyadmin?

Reply

Hey Maxim,

Oh, I see what you mean. Well, it's a good question... but probably that's not something that works out of the box with EasyAdmin, I suppose you would need to do some extra work for this :) I bet you need a "signed URLs" feature... you can implement it yourself probably, or try some API, e.g. Amazon S3 has signed URLs IIRC. But you're thinking correct, you need to store all those images outside of your document root, i.e. public/ directory, otherwise someone still can guess the URL and download it. Signed URLs will allow you to give access only to specific people or e.g. after a successful purchase, and only for a limited amount of time.

I hope this helps!

Cheers!

Reply
Tien dat L. Avatar
Tien dat L. Avatar Tien dat L. | posted 1 year ago

Hi, for ImageField in Edit-Action is attribute 'required' -> true, how can i handle this attribute like if(avatar) then required->false else required->true.

I try to do : ->setFormTypeOptions(['required' => false]) then is fine in Edit-Action but if i deleted file then got error

Reply

Hey Tien dat L.!

Hmm, that's interesting - I hadn't noticed that. I would have solved this the same way you did, with: ->setFormTypeOptions(['required' => false]). What error do you get on delete? For the most part, the "required" option controls whether or not a "required" attribute is rendered on the HTML field... but I'm surprised to hear that it would cause different behavior on the backend (the error).

Let me know :).

Cheers!

Reply
Peter A. Avatar
Peter A. Avatar Peter A. | posted 1 year ago

Hi,

I do want to use Symfony UX Dropzone to upload multiple files (documents-type) with easyadmin. Can i get some guidance on how to go about it. ? thanks

Reply

Hey Peter,

We have a separate tutorial when we're talking A LOT about file uploads in Symfony: https://symfonycasts.com/sc... - please, check it out! In particular, we're talking about Dropzone there too, here's the link to the specific screencast: https://symfonycasts.com/sc... - but I'd recommend you to watch the whole tutorial to have more context around. Along with that tutorial and EasyAdmin tutorial, I believe you will solve your problem.

Cheers!

Reply
Default user avatar

Hi, I have a problem with this field in edit action. My image field is required and during edit I must upload new file even though there already exists one that I uploaded when I created my entity and I did not delete it. Could you tell me why is it happening?

Reply

Hey Otix12,

Did you download the course code and start from the start/ directory? Or are you trying to follow this course on your own project?

Cheers!

1 Reply
henryosim Avatar
henryosim Avatar henryosim | posted 1 year ago

I am using imageField to upload files in EA. but i cant seem to find the actual UploadedFile (even when i listen to BeforeEntityPersistedEvent).

Questions:
* how can i validate the UploadedFile mimeType?
* how can i get the UploadedFile so I can resize(if it's an image)?

with VichUploaderBundle i could read the $imageFile field but i am struggling to find the File using EasyAdmin imageField. Please Help:pray:

Reply

Hey Anabs,

First of all, you can pass callable to the setUploadedFileNamePattern() that will give you (UploadedFile $file) in case you want to get access to it in that spot. Otherwise, I suppose you want to override parent protected processUploadedFiles() method that has "FormInterface $form" argument. Please, try to dump that variable, I bet you can get that UploadedFile from there. And this is a good spot to validate the form and resize the image IMO.

I hope this helps!

Cheers!

Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 1 year ago | edited

Hey @disqus_uUZdE4XYyx

We didn't cover that part but remember, EasyAdminBundle uses the Symfony Form component under the hood. So, you can easily add any File constraints to your file field. I'll leave you a link to the docs where you can read how to configure the form options: https://symfony.com/doc/cur...

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0
    }
}
userVoice