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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "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
    }
}