If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOpen 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.
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 theuploads/
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.
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
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.
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.
// 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
}
}