Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

File Uploads & Data Fixtures

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

Open up src/DataFixtures/ArticleFixtures.php. Here's how this works: this function creates 10 articles whenever we run bin/console doctrine:fixtures:load. It's a cool helper we created in our Symfony series. But, the setImageFilename() stuff is now a problem. We know that the image filename needs to be the name of a file that lives inside of the uploads/article_image directory - something like astronaut-blah-blah.jpg. Right now, the fixtures use faker to select a random item in $articleImages - this private property. So, it's setting imageFilename to either asteroid.jpeg, mercury.jpeg or lightspeed.png.

This worked before because those images are committed to our repository in the public/images directory and we were pointing to that path in our template. When we run doctrine:fixtures:load, it does create 10 Article objects and it does set the image filename to one of these three filenames. But on the homepage... it doesn't work! There is no upload/article_image/lightspeed.png file. We need to re-think how this works.

Faking the File Upload

How? By faking the file upload inside the fixtures. It's kinda...beautiful! Our UploaderHelper service is already really good at moving things into the right spot - why not reuse it here?

Inside ArticleFixtures, create a public function __construct(). Add an UploaderHelper $uploaderHelper argument and I'll hit ALT + Enter and select initialize fields to create that property and set it.

... lines 1 - 7
use App\Service\UploaderHelper;
... lines 9 - 12
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 15 - 26
private $uploaderHelper;
public function __construct(UploaderHelper $uploaderHelper)
{
$this->uploaderHelper = $uploaderHelper;
}
... lines 33 - 90
}

Next, lets "cut" the 3 files in the public/images directory: we're going to move them to a different spot, because they no longer need to be publicly accessible. You'll see what I mean. In the src/DataFixtures directory, create a new folder here called images/ and paste them! Yep! They are no longer in the public/images/ directory.

Because these test images are committed to git, I'm going to commit this move - it'll help us in a minute when things... ah... sorta go wrong horribly wrong. Yes! We are planning for disaster!

Here's the idea: we'll use the UploaderHelper down here, point it at one of these 3 files, and have it, sort of, "fake" upload it. Start with $randomImage =, copy the faker code, and paste. This is now one of the three random image filenames.

... lines 1 - 12
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 15 - 33
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 37 - 63
$randomImage = $this->faker->randomElement(self::$articleImages);
... lines 65 - 78
});
... lines 80 - 81
}
... lines 83 - 90
}

Next, in UploaderHelper, what I'd like to do is call uploadArticleImage() and basically say:

Hey! Pretend like asteroid.jpeg is a file that was just uploaded. And... ya know... do all your normal stuff and move it into the uploads/ directory.

This is easier than you think: in the fixtures class, set $imageFilename to $this->uploaderHelper->uploadArticleImage(). What I want to do is now say new UploadedFile() and point it at one of the images. The problem is that you can't really create a fake UploadedFile object. Internally, it's bound to the PHP uploading process - weird stuff will happen if you try to create one outside of that context.

Hello File Object

That's ok! It just means we need to dig deeper! Go back into UploaderHelper. Hold Command or Ctrl and click to open the UploadedFile class. This lives in the Symfony\HttpFoundation\File namespace and extends a class called File that lives in the same directory.

The File class is awesome: it simply represents... any file on your filesystem, regardless of whether it's an uploaded file or just a normal file. And, if you look closely, the vast majority of the methods we've been using come from this class - not from UploadedFile. And we can create a File object outside of an upload context.

So back in ArticleFixtures, instead of creating a new UploadedFile(), say new File() - the one from HttpFoundation. Pass this the path to the random image: __DIR__.'/images/' and then $randomImage, which will be one of these image filenames.

... lines 1 - 10
use Symfony\Component\HttpFoundation\File\File;
... line 12
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 15 - 33
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 37 - 63
$randomImage = $this->faker->randomElement(self::$articleImages);
$imageFilename = $this->uploaderHelper
->uploadArticleImage(new File(__DIR__.'/images/'.$randomImage));
... lines 67 - 78
});
... lines 80 - 81
}
... lines 83 - 90
}

Now, take $imageFilename - that'll be whatever the final filename is on the system after moving it, and set that onto the entity.

That's beautiful! In UploaderHelper, we need to make this work not with an UploadedFile object, but with the parent File. Change the type-hint to File - again, make sure you get the one from HttpFoundation or you will have no fun. To keep things clear, I'll Refactor -> Rename this variable to $file.

... lines 1 - 5
use Symfony\Component\HttpFoundation\File\File;
... lines 7 - 9
class UploaderHelper
{
... lines 12 - 23
public function uploadArticleImage(File $file): string
{
... lines 26 - 34
$file->move(
$destination,
$newFilename
);
... lines 39 - 40
}
... lines 42 - 48
}

Let's see: everything looks happy, ah - except for getClientOriginalName(): that method does not exist in File - it only exists in UploadedFile. Ok, let's get fancy then: if $file is an instanceof UploadedFile, we can say $originalFilename = $file->getClientOriginalName(). Else, set $originalFilename to $file->getFilename() - that's just the name of the file on the filesytem.

... lines 1 - 5
use Symfony\Component\HttpFoundation\File\File;
... lines 7 - 9
class UploaderHelper
{
... lines 12 - 23
public function uploadArticleImage(File $file): string
{
... lines 26 - 27
if ($file instanceof UploadedFile) {
$originalFilename = $file->getClientOriginalName();
} else {
$originalFilename = $file->getFilename();
}
... lines 33 - 40
}
... lines 42 - 48
}

After this, delete the pathinfo() stuff - we can move that to the next line. Inside urlize(), re-add the pathinfo() and pass the same second argument: PATHINFO_FILENAME.

... lines 1 - 5
use Symfony\Component\HttpFoundation\File\File;
... lines 7 - 9
class UploaderHelper
{
... lines 12 - 23
public function uploadArticleImage(File $file): string
{
... lines 26 - 27
if ($file instanceof UploadedFile) {
$originalFilename = $file->getClientOriginalName();
} else {
$originalFilename = $file->getFilename();
}
$newFilename = Urlizer::urlize(pathinfo($originalFilename, PATHINFO_FILENAME)).'-'.uniqid().'.'.$file->guessExtension();
... lines 34 - 40
}
... lines 42 - 48
}

I think that's all we need! Let's completely clear out the uploads/ directory. Now, find your terminal and run:

php bin/console doctrine:fixtures:load

Copying the Files Before Moving

Woh! The file src/DataFixtures/images/asteroid.jpeg does not exist? Hmm. Check this out: it did upload two files before going all "explody" on us. Oh, but those original files are missing! Of course! We're using $file->move(). So it is working, but instead of copying the files, it's moving them, and the originals are disappearing.

Let's get those files back. Run:

git status

And undelete them with:

git checkout src/DataFixtures/images

Much better. Let's clean out the uploads directory again.

We do want to use $file->move() because we do want to move the uploaded file in normal circumstances. So, to get around this, in the fixtures, let's copy the original file to a temporary spot. Start with $fs = new Filesystem() - that's a handy object for doing filesystem operations.

... lines 1 - 10
use Symfony\Component\Filesystem\Filesystem;
... lines 12 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 34
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 38 - 64
$randomImage = $this->faker->randomElement(self::$articleImages);
$fs = new Filesystem();
... lines 67 - 82
});
... lines 84 - 85
}
... lines 87 - 94
}

Next, $targetPath = sys_get_temp_dir().'/'.$randomImage. And then use $fs->copy(). We want to copy the original file path into $targetPath.

... lines 1 - 10
use Symfony\Component\Filesystem\Filesystem;
... lines 12 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 34
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 38 - 65
$fs = new Filesystem();
$targetPath = sys_get_temp_dir().'/'.$randomImage;
$fs->copy(__DIR__.'/images/'.$randomImage, $targetPath, true);
... lines 69 - 82
});
... lines 84 - 85
}
... lines 87 - 94
}

Inside File, pass the temporary path.

... lines 1 - 10
use Symfony\Component\Filesystem\Filesystem;
... lines 12 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 34
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 38 - 65
$fs = new Filesystem();
$targetPath = sys_get_temp_dir().'/'.$randomImage;
$fs->copy(__DIR__.'/images/'.$randomImage, $targetPath, true);
$imageFilename = $this->uploaderHelper
->uploadArticleImage(new File($targetPath));
... lines 71 - 82
});
... lines 84 - 85
}
... lines 87 - 94
}

Ok, let's try it again!

php bin/console doctrine:fixtures:load

No error, our original files still exist and... we have a directory full of, fake uploaded files. Now try the homepage. Beautiful. What I really love about this is that we're not doing anything fancy or tricky in our fixtures: we're literally using our upload system.

Cleanup into a Private Method

Though, I don't love having all of this logic right in the middle of this already-long function: it's not super obvious what it does. Let's do some cleanup: copy all of this. And at the bottom, create a new private function fakeUploadImage() that will return a string.

... lines 1 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 90
private function fakeUploadImage(): string
{
... lines 93 - 99
}
}

Paste all that logic and return the $this->uploaderHelper line. It selects a random image, uploads it and returns the path.

... lines 1 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 90
private function fakeUploadImage(): string
{
$randomImage = $this->faker->randomElement(self::$articleImages);
$fs = new Filesystem();
$targetPath = sys_get_temp_dir().'/'.$randomImage;
$fs->copy(__DIR__.'/images/'.$randomImage, $targetPath, true);
return $this->uploaderHelper
->uploadArticleImage(new File($targetPath));
}
}

Back up top, delete all this stuff and say $imageFilename = $this->fakeUploadImage().

... lines 1 - 13
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface
{
... lines 16 - 33
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_articles', function($count) use ($manager) {
... lines 38 - 64
$imageFilename = $this->fakeUploadImage();
... lines 66 - 77
});
... lines 79 - 80
}
... lines 82 - 100
}

Let's run those fixtures one more time!

php bin/console doctrine:load:fixtures

When it finishes... we have some new files... and the homepage is shiny! That's a solid fixture system.

Next: we'll take our first step towards storing uploaded files in the cloud by integrating the gorgeous Flysystem library.

Leave a comment!

11
Login or Register to join the conversation
Default user avatar
Default user avatar Aurélien Dachier | posted 1 year ago

Hello,

When I follow the steps, i got an error saying "The service "App\Service\UploaderHelper" has a dependency on a non-existent service "uploadsPath"." Can anyone help me ?

Reply

Hey Aurélien Dachier!

Hmmm. Do you have any config in your services.yaml file for UploaderHelper? And also, does your "bind" line look like this? https://symfonycasts.com/screencast/symfony-uploads/uploader-service#codeblock-9d72416900

Usually, this error would make me think that you have some config that makes it look like "uploadsPath" is a service - some config like:


services:
    _defaults:
        bind:
            $uploadsPath: '@uploadsPath'

Or perhaps:


services:
    App\Service\UploaderHelper:
        arguments:
            $uploadsPath: '@uploadsPath'

Let me know what you see in your services.yaml file :).

Cheers!

1 Reply
Default user avatar
Default user avatar Aurélien Dachier | weaverryan | posted 1 year ago | edited

Hi ! Thanks for your answer, here's what i have in my services.yaml file :


parameters:
    app.path.product_images: /uploads/images/products
    app.path.event_image: /uploads/images/events

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
            - '../src/Tests/'

    App\Service\UploaderHelper:
        class: App\Service\UploaderHelper
        arguments: 
            $uploadsPath: '@uploadsPath'
            $requestStackContext: '@requestStackContext'

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

Did I make something wrong ?

Reply

Hey Aurélien Dachier!

Excellent - this is very useful! When you have the @uploadsPath value in this file, it tells Symfony to look for a service whose id is uploadsPath, which is not what you're intending. My guess is that you actually want to pass in one of those two parameters. In that case, you would use something like $uploadsPath: '%app.path.product_images%'.

Let me know if that helps :).

Cheers!

Reply
Jean M. Avatar
Jean M. Avatar Jean M. | posted 3 years ago

Hello,

When i learned for the first time how to uploading file, it was recommended by Fabien Potencier to create an entity for the file and manage the UploadeFile class in the entity with doctrine events, for example App\Entity\Image. Today, i always follow this paradigm for my apps and manage all the files as mapped relations with Doctrine, so the approach of this tutorial perturbs me. Do you consider is not a good practise ? if yes, why ?

Source (fifth item) : https://openclassrooms.com/...

Regards

Reply

Hey Jean M.!

I think it's a fine approach :). It's not the one I prefer - but there is not super important reason for that. The thing I don't like about the entity solution (though, again, it's not such a major detail) is that it requires you to have a property on your entity that exists *just* to temporarily hold the UploadedFile - it's not meant to be persisted. Using an unmapped field in a form removes the need for this property.

The other thing that I don't prefer with lifecycle callbacks for this, is that it requires all of your logic to be inside of your entity - but I would usually prefer to have this logic in a service. This also limits you to saving things to the local filesystem - you can't use something like Flysystem, because you can't access services from inside your entity.

So, I see the lifecycle events approach to file uploading as a simpler, easier way of doing things. But I personally prefer to split things up a bit more, just because I like to have the logic in a service. But it sounds like you have had a lot of luck with the simpler approach - which does not surprise me. So if you want to keep doing it, no complaints from me ;).

Cheers!

1 Reply
Jean M. Avatar

Ok, it was an old tutorial, an easier way for basic usage i think but services are better way for customization. No problem for changing my paradigm. So i continue the course.

Thanks!

Reply

5:50 Can't you just use $test argument of UploadedFile constructor? It seems to be not marked as internal. Personally I don't like messing up my code logic just for fixtures. :)

Reply

Hey TomaszGasior

That's a good point, I think you could use that flag argument if you want but I don't see the uploading logic being polluted with fixtures logic, all the pesky details of copying files live inside fixtures

Cheers!

Reply
infete Avatar

A big THANKS for this chapter.

Reply

Hey Manoj,

Thank you for your feedback! We're really happy you like it and it's useful for you ;)

Cheers!

1 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
    }
}
userVoice