Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

multipart/form-data and FormType validation

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).

like image 411
Malachi Avatar asked Aug 15 '14 13:08

Malachi


3 Answers

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--
like image 157
rolebi Avatar answered Nov 18 '22 12:11

rolebi


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>
like image 41
Dmitrii Korotovskii Avatar answered Nov 18 '22 13:11

Dmitrii Korotovskii


After giving up and looking at an alternative option of having a separate endpoint for the image upload. For example:

  1. Create the new comment.

POST /comments

  1. 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/
like image 34
Malachi Avatar answered Nov 18 '22 13:11

Malachi