multipart/form-data and FormType validation

2020-05-27 05:43发布

问题:

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

回答1:

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


回答2:

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>


回答3:

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/


回答4:

Modify the app to send the file content in the JSON.

  1. Read the file content in you app.
  2. Base64 encode the content of the file
  3. Create a JSON with all your field (included the one with the file content)
  4. Send the JSON to the server.
  5. 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=='
}