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
).
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--
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>
After giving up and looking at an alternative option of having a separate endpoint for the image upload. For example:
- Create the new comment.
POST /comments
- Upload image to end point
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.
- http://sroze.github.io/SRIORestUploadBundle/
Modify the app to send the file content in the JSON.
- Read the file content in you app.
- Base64 encode the content of the file
- Create a
JSON
with all your field (included the one with the file content)
- Send the
JSON
to the server.
- Handle it in the standard way.
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=='
}