Chapters
40 Chapters
|
4:39:58
|
Login to bookmark this video
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3
Subscribe to download the code!Compatible PHP versions: ^7.1.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
02.
Uploads, multipart/form-data & UploadedFile
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Subscribe to jump to this part in the video!
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
"doctrine/annotations": "^1.0", // 1.10.1
"doctrine/doctrine-bundle": "^1.6.10", // 1.10.2
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
"doctrine/orm": "^2.5.11", // v2.7.2
"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
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.0
"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.21.6
"symfony/form": "^4.0", // v4.2.3
"symfony/framework-bundle": "^4.0", // v4.2.3
"symfony/property-access": "4.2.*", // v4.2.3
"symfony/property-info": "4.2.*", // v4.2.3
"symfony/security-bundle": "^4.0", // v4.2.3
"symfony/serializer": "4.2.*", // v4.2.3
"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/stopwatch": "4.2.*", // v4.2.3
"symfony/var-dumper": "^3.3|^4.0", // v4.2.3
"symfony/web-profiler-bundle": "4.2.*" // v4.2.3
}
}
16 Comments
Hey there ,
I was wondering if there are any ways of retrieving some additional data from the request body, using the UploadedFile class. I'm sending the file from frontend with dropzone, which adds additional props to the file object that is sent to the backend, like a generated uuid. Is there anyway i can get that directly in backend? something like $uploadedFile->getUuid()? I've manually added that uuid as an additional param in the request body, but it feels like it's duplicated since i'm already sending the file object and i should have access to it, yet only few props are available
Hey Petru,
Hm, first of all, there's no any dynamic methods, so you can't call getUuid() method on the UploadedFile instance unfortunately. But it has getContent() method, you can try to dump the content of it to see if you have that UUID value there. Probably, with some extra steps you can decode and get that value from the content. Another possible way, I think you can create a custom class that will extends UploadedFile from core, or probably better create a wrapper for UploadedFile instance - you will pass the instance of UploadedFile to it's constructor and that class will help with extracting additional info and have methods like getUuid().
Or, try to get the current Request object and look at the values it has. All additional information about the request should be there. I think dumping the request with dd($request) should show all the information it has. Then, after you will know where that extra info is stored in the Request object - you will know how to get that extra data from the request.
I hope this helps!
Cheers!
Hi. how to add an authorization header? thanks
Hey Arwa,
It depends on the implementation you have on your server side. In this video we're authorized and information about the current logged in user is in session. That's why we don't need to send any authorization headers. But if you're talking about not using sessions - you probably does not upload the file via usual HTML form. If you send it via the Curl or via some apps like Postman - check the docs how to add headers there. But still, you need to know how your server side works to know what exactly headers to add.
I hope this helps.
Cheers!
Hello team! I have seen this tutorial I think a bit late, and well I think I started at the end... or not?! ... Is this tutorial applicable to API Platform context? In other words, the way to upload files in API Platform can be done the same as in this tutorial? Because I have been reading the API Platform documentation and I have seen that it is quite different, I have also been watching this tutorial and I have not found a way to use it correctly in the context of API Platform, also I have not found the correct way to make Fixtures in the same context. Is this part of another tutorial or is it that I have not understood this part well yet? I would appreciate a help, Thanks in advance!!
Hey Toshiro!
Sorry for the slow reply :). Excellent question! As you can see from the API Platform docs, handling uploads in API Platform is really a custom thing. And, to a point, I think the correct implementation depends on your exact situation. First, they advocate to use VichUploaderBundle. That's a find bundle, but I personally don't use it (it feels "heavy" to me), but there is absolutely nothing wrong with it.
Could you describe your use-case a bit so I can recommend an approach? The example in API Platform - which uses a custom controller - is a bit ugly, but it works well. And it can be used with or without Vich. If you don't use Vich, then you would basically do what we do in this tutorial inside that custom controller. Another option, depending on your situation, is to leave the "post" operation alone (don't override it and give it a custom controller) but instead to add a *new* operation (e.g. "uploadImage") that handles only the upload (and then it would have a custom controller). This is a nice way to organize things, but it would mean that you would need to API calls to fully populate your object - e.g. a POST to /api/products (to use products as an example) to send all the product data and then another POST to /api/products/image to send just the file upload. Sometimes this is not feasible for whoever/whatever is using your API, but sometimes this is *desired*, as you can easily 2 both actions (editing data and the file upload) independently.
Anyways, I'm not sure I've answered your question or not yet - let me know ;). I am not convinced that there is one approach that is always best. And one thing I don't like about the API Platform solution is that, if you override the "post" operation with a custom controller, then it doesn't work if you have an API resource that holds other fields (like Product above, where you also want to send fields like title, price, etc in JSON, but also have an upload field). That's why separating into 2 custom controllers is a good option for that case.
Cheers!
Hello weaverryan thanks for the reply! This is how I imagined it would be ... It's good to see that I was not far from the truth. Well my use case is the following: I have services on my site that each have an image, but several services can have the same image so it would be a many-to-one relationship of service to MediaObject, as well as the business model can grow later, it is possible that other entities also have other relationships with my MediaObject entity (this is thinking about the future), so I have a public and a private filesystem with the OneupFlysystemBundle because some of those future relationships may be private. The VichBundle documentation has not really helped me much, in terms of how to annotate the entities to be able to cascade "persist" the images in the POST operation for example of the Service entity, so I decided to take a risk and use other variants, for example a DataPersister and a DataProvider (for fill the contentUrl field), or maybe DTOs for these entities ... for now I'm testing. What do you think is the best variant? I came up with these solutions thinking that when I do a GET to /api/services the contentUrl field will be embedded in the json of each of the services and when I made the first DataProvider for MediaObject I realized that it did not fill the contentUrl field when I made this request ... I imagined that this DataProvider was not called, now I am implementing a DataProvider for Services to fill the contentUrl field in the GET requests that I make to the Service entity. By the way, the same thing happened to me when I created a user through another entity, for example I have an Appointment entity and an anonymous user can create an appointment through the POST operation of this entity, and the way I seek to solve this problem was to create it a temporary user (who has not been registered yet), that user needs a password but when I persisted it in a normal cascade (letting doctrine do its normal work), it did not encrypt the password field so.... I had to create a DataPersister for that entity also to encrypt the password there... When I finish any of the solutions and it is working correctly and above all "have no doubts about it" I will share it here since I really have not found any example of this anywhere!. Thanks for your help Ryan I'm learning like A LOT!!! thanks to all the team!!!
Hey Toshiro!
Sorry again for my slow reply!
I DO like the idea of being able to make a GET request to /api/services, and having a contentUrl property there, even if in reality, the Service has a ManyToOne to MediaObject and THAT holds the information about where the image is stored :). It's nice to me (but subjective) then returning an embedded "media" field with another embedded "contentUrl" field. But, that just depends on your taste, and if there were other important fields on MediaObject that you wanted to expose, then it makes more sense :).
You mentioned that the data provider was not called on MediaObject when you make a request to /api/services. That is correct. The data provider is only called to "fetch" the "top-level" resource. In this case, the "Service" object. Once that has been fetched, even if you exposed a "mediaObject" field, API Platform simply calls
$service->getMediaObject()to fetch that item. There is no data provider involved. We talk a bit about that here - https://symfonycasts.com/screencast/api-platform-security/filtered-collection - that's a collection property, but we talk about how the method is called directly, there is no extra query or data provider called for this. So, if you did want to add a mediaObject.contentUrl field to your service, that logic would need to live in the data provider for the Service: it would call ->getMediaObject() and then populate the contentUrl field on that object.By the way, if you have a MediaObject relation on several entities and want to convert all of them to have some contentUrl field, you could create an interface - e.g. HasMediaObjectInterface - with a getMediaObject(): ?MediaObject method on it. Then, you could create a custom data provider that operates on all classes with this interface. The only tricky part (and this IS unnecessarily tricky currently) is that you would need to inject the entire "data provider" system and call it after you added the field so that the "normal" data provider could be called (in case you already have other custom data providers for those classes). To avoid recursion, your supports method would need to somehow know that it has already been called and return false the second time. It's kind of annoying :/.
Yep, this is the same problem with the data providers! The data persister is only called for the "top level" object that you are persisting - it's not called for any sub-items :).
Cheers and good luck!
It was really difficult to come up with a working example because the documentation is not very clear on this issue and I am not that good with api platform yet so..., weaverryan what do you think of this solution? Do you have any suggestion? I hope this help someone!! Cheers!!!
Well it seems that my solution was not uploaded correctly ... I think I'd better do an example on my Github page and share the link, it seems that it was too long ...
Hey Toshiro!
Yea... I don't see it here - sorry about that :/. I you link to something else, like GitHub, I can check it out.
Cheers!
Hi there! Can we specify the "temp" directory on the config? IN my case is "/tmp", but on the video is another. So it is configurable? Thanks!
Hey @Alex
Are you getting an error when uploading a file? I'm asking because you should not worry about the default temporary directory. Symfony uses the built-in php function that detects the temporary directory based on your OS
Cheers!
Hi!
No I'm not getting any error, just asking because that difference!
Thanks for clarifying!
Regards
Thx Ryan! Nice stuff as always!! I really appreciate that! I have one question out of curiosity, what kind of tool you have used to dump multipart/form-data request?
Hey @CuriousPete!
I like that you were interested enough to ask that question :). I used Charles - the debugging proxy - it catches all traffic and you can see the raw body :).
Cheers!
"Houston: no signs of life"
Start the conversation!