gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
We've got a pretty nice system so far: moving the file, unique filenames and putting the filename string into the database. But it is kind of a lot of logic to put in the controller... and we already need to reuse this code somewhere else: in the new()
action.
That's why I like to isolate my upload logic into a service class. In the Service/
directory - or really anywhere - create a new class: how about UploaderHelper
?
namespace App\Service; | |
... lines 4 - 6 | |
class UploaderHelper | |
{ | |
... lines 10 - 21 | |
} |
This class will handle all things related to uploading files. Create a public function uploadArticleImage()
: it will take the UploadedFile
as an argument - remember the one from HttpFoundation
- and return a string
. That will be the string filename that was ultimately saved.
... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\File\UploadedFile; | |
... line 7 | |
class UploaderHelper | |
{ | |
public function uploadArticleImage(UploadedFile $uploadedFile): string | |
{ | |
... lines 12 - 20 | |
} | |
} |
Ok! Let's go steal some code for this. In fact, we're going to steal pretty much all the logic here... and paste it in. Make sure to retype the r
on Urlizer
to get the use
statement on top.
... lines 1 - 4 | |
use Gedmo\Sluggable\Util\Urlizer; | |
... lines 6 - 7 | |
class UploaderHelper | |
{ | |
public function uploadArticleImage(UploadedFile $uploadedFile): string | |
{ | |
$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 | |
); | |
} | |
} |
And at the bottom, return $newFilename
.
... lines 1 - 7 | |
class UploaderHelper | |
{ | |
... lines 10 - 16 | |
public function uploadArticleImage(UploadedFile $uploadedFile): string | |
{ | |
... lines 19 - 28 | |
return $newFilename; | |
} | |
} |
Perfect! Well... not perfect, because the $this->getParameter()
method is a shortcut that only works in the controller. If you need a parameter - or any configuration - from inside a service, you need to add it via dependency injection. Add the public function __construct()
with, how about, a string $uploadsPath
argument. Instead of just injecting the kernel.project_dir
parameter, we'll pass in the whole string to where uploads should be stored.
... lines 1 - 7 | |
class UploaderHelper | |
{ | |
private $uploadsPath; | |
public function __construct(string $uploadsPath) | |
{ | |
$this->uploadsPath = $uploadsPath; | |
} | |
... lines 16 - 30 | |
} |
I'll put my cursor on that argument name, hit Alt + Enter
and select initialize fields to create that property and set it. Now, below, we can say $this->uploadsPath
and then /article_image
.
... lines 1 - 7 | |
class UploaderHelper | |
{ | |
... lines 10 - 16 | |
public function uploadArticleImage(UploadedFile $uploadedFile): string | |
{ | |
$destination = $this->uploadsPath.'/article_image'; | |
... lines 21 - 29 | |
} | |
} |
Cool! Let's worry about configuring the $uploadsPath
argument to our service in a minute. After all, Symfony's service system is so awesome, it'll tell me exactly what I need to configure once we try this.
For now, go back into ArticleAdminController
and use this. Start by adding another argument: UploaderHelper $uploaderHelper
. And celebrate by removing all of the logic below and replacing it with $newFilename = $uploaderHelper->uploadArticleImage($uploadedFile)
.
... lines 1 - 7 | |
use App\Service\UploaderHelper; | |
... lines 9 - 17 | |
class ArticleAdminController extends BaseController | |
{ | |
... lines 20 - 49 | |
public function edit(Article $article, Request $request, EntityManagerInterface $em, UploaderHelper $uploaderHelper) | |
{ | |
... lines 52 - 56 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 58 - 59 | |
if ($uploadedFile) { | |
$newFilename = $uploaderHelper->uploadArticleImage($uploadedFile); | |
$article->setImageFilename($newFilename); | |
} | |
... lines 64 - 72 | |
} | |
... lines 74 - 77 | |
} | |
... lines 79 - 116 | |
} |
Dang - that is nice! There is still a little bit of logic here: the form logic and the logic that sets the filename on the Article
- but I'm comfortable with that. And we now have this great new method: pass it an UploadedFile
object, and it'll move it into the correct directory and give it a unique filename.
Let's take it for a test drive! Go back, refresh the form and... it works! Naw, I'm kidding - we knew this error was coming... but isn't it beautiful?
Cannot resolve argument
$uploadHelper
of theedit()
method: Cannot autowire serviceUploadHelper
: argument$uploadsPath
of method__construct()
is type-hintedstring
, you should configure its value explicitly.
That's programming poetry people! And it makes sense: autowiring doesn't work for scalar arguments. We got this: open config/services.yaml
. We could configure the specific argument for this specific service. But if you've watched our Symfony series, you know that I like to use the bind
feature. The argument name is $uploadsPath
. So, below _defaults
and bind
, add $uploadsPath
set to %kernel.project_dir%/public/uploads
.
... lines 1 - 9 | |
services: | |
... line 11 | |
_defaults: | |
... lines 13 - 19 | |
bind: | |
... lines 21 - 22 | |
$uploadsPath: '%kernel.project_dir%/public/uploads' | |
... lines 24 - 47 |
This means: anywhere that $uploadsPath
is used as an argument for a method that's autowired - usually a controller action or the constructor of a service - pass in this value.
Let's go see if that fixed things - reload. Now we see the form. To test this fully, let's empty out the article_image/
directory. This time, let's upload the stars photo. Hit update.
Woh! The file "empty string" does not exist!? What the heck! Let's do some digging. When we call guessExtension()
, internally, Symfony looks at the contents of the temporary uploaded file to determine what's inside. But... that file is missing! In fact, PHP is telling us that the temporary file name is... an empty string! It's madness!
Why is this happening? I'll give you a clue: the file we just uploaded is 3mb. Go to your terminal and run
php -i | grep upload
There it is: the upload_max_filesize
in my php.ini
is 2 megabytes, which is PHP's default value. I have a bunch of things to say about this. First, make sure you set this value to whatever you really want your max to be. You may also need to bump the post_max_size
setting - that defaults to 8 mb, and also will cause uploads to fail if they're bigger than this.
Second, if you're getting super weird results while uploading, this is probably the problem. And third, once we add validation to our upload field, we'll get a really nice validation error instead of this crazy fatal error. Symfony has our back.
So let's try a smaller file - our astronaut - it's 1.9 mb. Hit update and... yes! It worked!
Now that all of our logic is isolated, we can easily repeat this in the new()
action. We do need to copy these 5 lines or so, but I'm happy with that.
Up in new()
, add the argument - UploaderHelper $uploaderHelper
- and inside the isValid()
block, paste!
... lines 1 - 17 | |
class ArticleAdminController extends BaseController | |
{ | |
... lines 20 - 23 | |
public function new(EntityManagerInterface $em, Request $request, UploaderHelper $uploaderHelper) | |
{ | |
... lines 26 - 28 | |
if ($form->isSubmitted() && $form->isValid()) { | |
/** @var Article $article */ | |
$article = $form->getData(); | |
/** @var UploadedFile $uploadedFile */ | |
$uploadedFile = $form['imageFile']->getData(); | |
if ($uploadedFile) { | |
$newFilename = $uploaderHelper->uploadArticleImage($uploadedFile); | |
$article->setImageFilename($newFilename); | |
} | |
... lines 40 - 46 | |
} | |
... lines 48 - 51 | |
} | |
... lines 53 - 124 | |
} |
This uses the same form, with the same unmapped field, so it'll all just work.
Next: let's talk about validation.
Hi there,
Great tutorials, thanks.
Please can you explain me why in controllers we can just use $this->getParameter('kernel.project_dir') and why in a service, we need to use depedency injection ? I mean why fundamentally ?
Hey Simon L.
You can do things like $this->getParameter('kernel.project_dir')
in a controller because a controller has access to the Symfony Container. The new standard is to rely on the container as less as possible, that's why we always prefer to inject services, this give us clarity of the dependencies of our services. Also, is possible that you want to instantiate a service with different configuration, i.e. different arguments, so, having said that I recommend you to inject services/parameters as much as possible
Cheers!
Hello,
The only problem I have so far is (every time I hit the Update! Button) that it clears the Content field of the form.
No errors at all.
I've downloaded the course and set it up.
When you edit a form by the /admin/article/{id}/edit section and you hit update then the content of the field 'Content' will be erased.
Interesting... Can you confirm if other fields are saving after submit? Also, double check that you're passing the $article object as second argument to your form $form = $this->createForm(ArticleFormType::class, $article);
. When you do that the form autocompletes with the object you passed in.
Btw. when you submit, the controller redirects you back to the same page so you can see the applied changes.
Great tutorial. Only one small detail, a method that has side effects should not return a value, so better to create two functions: getFileName(UploadFile $file) : string, upload(UploadFile $file, string $fileName) : void. But this is probably not important for the explanation of the file upload logic.
Hey Stefan D.!
I think that's a very fair critique :). It's a bit more convenient to "smash" it all into one, but if you like having 2 better, I agree! And of course, having 2 is more flexible (and I would definitely do it that way if this were a reusable lib).
Cheers!
I get this problem:
Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?)
Don't know why, ps: I'm using windows
Hey Giacomo,
Hm, that's interesting... But do you have "fileinfo" PHP extension as it says? You can check it with:
$ php -m | grep fileinfo
Probably mime type guessers require this extension for correct work. Please, install it first and try again.
Cheers!
Hey Nizar
This course is about to be completely released, there are a bunch of episodes after this one, I believe you already know that, so could you be a bit more explicit about what you mean with "continuation of the course".
Cheers!
Hello,
It remains the last 5 lessons. usually you added the lessons every Wednesday and it's been more than a week that I'm waiting for these lessons
Am I clear or do you want more details yet?
Hey Ali!
Yes, we're still releasing it! And, actually, I see we release it *every day* not only on Wednesday - you can check it here: https://symfonycasts.com/up... . So, to recap - only 4 videos are left here, so this course will be completely released on the next week!
Thank you for your patience!
P.S. on the course overview page: https://symfonycasts.com/sc... - you can subscribe and we'll inform you by email (only 1 email) as soon as this is completely released. I hope this helps!
Cheers!
// 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
}
}
Hello,
I want to upload 3Mb xml file using Microservice. What I do wrong ? Got Error Serialization of 'Symfony\Component\HttpFoundation\File\UploadedFile' is not allowed
`
public function microBankTransactionImportIso20022(Request $request):RedirectResponse
{
$bankTransactionIso20022File = $request->files->get('bank-transaction-iso20022-file');
if ( $bankTransactionIso20022File instanceof UploadedFile)
{