Reputation: 33720
I am building an API using the FOSRestBundle and am at the stage where I need to implement the handling of the creation of new entities that contain binary data.
Following the methods outlined on Sending binary data along with a REST API request
sending the data as multipart/form-data
feels the most practical for our implementation due to the ~33% added bandwidth required for Base64.
Question
How can I configure the REST end point to both handle the file within the request and perform validation on the JSON encoded entity when sending the data as multipart/form-data
?
When just sending the raw JSON I have been using Symfony's form handleRequest
method to perform validation against the custom FormType
. For example:
$form = $this->createForm(new CommentType(), $comment, ['method' => 'POST']);
$form->handleRequest($request);
if ($form->isValid()) {
// Is valid
}
The reason I like this approach is so that I can have more control over the population of the entity depending whether the action is an update (PUT) or new (POST).
I understand that Symfony's Request
object handles the request such that previously the JSON data would be the content
variable but is now keyed under request->parameters->[form key]
and the files within the file bag (request->files
).
Upvotes: 9
Views: 9686
Reputation: 1181
It seems that there is no clean way to retrieve the Content-Type of the form-data without parsing the raw request.
If your API does support only json input or if you can add a custom header (see comments below), you can use this solution :
First you must implements your own body_listener
:
namespace Acme\ApiBundle\FOS\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use FOS\RestBundle\Decoder\DecoderProviderInterface;
class BodyListener
{
/**
* @var DecoderProviderInterface
*/
private $decoderProvider;
/**
* @param DecoderProviderInterface $decoderProvider Provider for fetching decoders
*/
public function __construct(DecoderProviderInterface $decoderProvider)
{
$this->decoderProvider = $decoderProvider;
}
/**
* {@inheritdoc}
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (strpos($request->headers->get('Content-Type'), 'multipart/form-data') !== 0) {
return;
}
$format = 'json';
/*
* or, using a custom header :
*
* if (!$request->headers->has('X-Form-Content-Type')) {
* return;
* }
* $format = $request->getFormat($request->headers->get('X-Form-Content-Type'));
*/
if (!$this->decoderProvider->supports($format)) {
return;
}
$decoder = $this->decoderProvider->getDecoder($format);
$iterator = $request->request->getIterator();
$request->request->set($iterator->key(), $decoder->decode($iterator->current(), $format));
}
}
Then in your config file :
services:
acme.api.fos.event_listener.body:
class: Acme\ApiBundle\FOS\EventListener\BodyListener
arguments:
- "@fos_rest.decoder_provider"
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
priority: 10
Finally, you'll just have to call handleRequest
in your controller. Ex:
$form = $this->createFormBuilder()
->add('foo', 'text')
->add('file', 'file')
->getForm()
;
$form->handleRequest($request);
Using this request format (form
must be replace by your form name):
POST http://xxx.xx HTTP/1.1
Content-Type: multipart/form-data; boundary="01ead4a5-7a67-4703-ad02-589886e00923"
Host: xxx.xx
Content-Length: XXX
--01ead4a5-7a67-4703-ad02-589886e00923
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name=form
{"foo":"bar"}
--01ead4a5-7a67-4703-ad02-589886e00923
Content-Type: text/plain
Content-Disposition: form-data; name=form[file]; filename=foo.txt
XXXX
--01ead4a5-7a67-4703-ad02-589886e00923--
Upvotes: 5
Reputation: 33720
After giving up and looking at an alternative option of having a separate endpoint for the image upload. For example:
POST /comments
POST /comments/{id}/image
I found there is already a bundle which provides various RESTful uploading processes. One of which was the one I originally wanted of being able to parse multipart/form-data
into an entity whilst extracting the file.
Upvotes: 1
Reputation: 1815
Modify the app to send the file content in the JSON.
JSON
with all your field (included the one with the file content)JSON
to the server.You get the file content in a base64 encoded string. You can then decode it and validate it.
Your JSON
will look like:
{
name: 'Foo',
phone: '123.345.678',
profile_image: 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw=='
}
Upvotes: 0
Reputation: 586
Here is more clear solution: http://labs.qandidate.com/blog/2014/08/13/handling-angularjs-post-requests-in-symfony/
Copy and pasting this code to other controllers is very WET and we like DRY!
What if I told you you could apply this to every JSON request without having to worry about it? We > wrote an event listener which - when tagged as a kernel.event_listener - will:
check if a request is a JSON request if so, decode the JSON populate the Request::$request object return a HTTP 400 Bad Request when something went wrong. Check out the code at https://github.com/qandidate-labs/symfony-json-request-transformer! Registering this event listener is really easy. Just add the following to your services.xml:
<service id="kernel.event_listener.json_request_transformer" > class="Qandidate\Common\Symfony\HttpKernel\EventListener\JsonRequestTransformerListener"> <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="100" /> </service>
Upvotes: 1