Проверка данных multipart/form-data и FormType

Я создаю API с использованием FOSRestBundle и нахожусь на этапе, когда мне нужно реализовать обработку создания новых объектов, которые содержат двоичные данные.

Следуя методам, описанным в разделе Отправка двоичных данных вместе с запросом REST API, отправка данных в виде multipart/form-data кажется наиболее практичным для нашей реализации из-за ~33% дополнительной пропускной способности, необходимой для Base64.

Вопрос

Как я могу настроить конечную точку REST для обработки файла в запросе и для проверки объекта в кодировке JSON при отправке данных как multipart/form-data?

Когда я просто отправлял сырой JSON, я использовал форму Symfony handleRequest метод для проверки по обычаю FormType, Например:

$form = $this->createForm(new CommentType(), $comment, ['method' => 'POST']);
$form->handleRequest($request);

if ($form->isValid()) {

  // Is valid

}

Причина, по которой мне нравится этот подход, заключается в том, что я могу больше контролировать заполнение сущности в зависимости от того, является ли действие обновлением (PUT) или новым (POST).

Я понимаю, что Symfony Request объект обрабатывает запрос так, что ранее данные JSON были бы content переменная, но теперь имеет ключ request->parameters->[form key] и файлы в папке с файлами (request->files).

4 ответа

Решение

После того, как вы отказались и посмотрели на альтернативный вариант иметь отдельную конечную точку для загрузки изображения. Например:

  1. Создайте новый комментарий.

POST /comments

  1. Загрузить изображение в конечную точку

POST /comments/{id}/image

Я обнаружил, что уже есть пакет, который обеспечивает различные процессы загрузки RESTful. Одним из которых был тот, который я изначально хотел иметь возможность анализировать multipart/form-data в сущности при извлечении файла.

Кажется, что нет чистого способа получить Content-Type данных формы без анализа необработанного запроса.

Если ваш API поддерживает только ввод json или вы можете добавить собственный заголовок (см. Комментарии ниже), вы можете использовать это решение:

Сначала вы должны реализовать свой собственный 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));
    }
}

Затем в вашем конфигурационном файле:

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

Наконец, вам просто нужно позвонить handleRequest в вашем контроллере. Пример:

$form = $this->createFormBuilder()
    ->add('foo', 'text')
    ->add('file', 'file')
    ->getForm()
;

$form->handleRequest($request);

Используя этот формат запроса (form необходимо заменить на имя вашей формы):

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

Вот более понятное решение: http://labs.qandidate.com/blog/2014/08/13/handling-angularjs-post-requests-in-symfony/

Скопируйте и вставьте этот код в другие контроллеры, очень влажно, и нам нравится DRY!

Что если я скажу вам, что вы можете применить это к каждому запросу JSON, не беспокоясь об этом? Мы> написали прослушиватель событий, который - когда помечен как kernel.event_listener - будет:

проверьте, является ли запрос JSON-запросом, если это так, декодируйте JSON, заполните объект Request::$request и верните HTTP 400 Bad Request, если что-то пошло не так. Проверьте код на https://github.com/qandidate-labs/symfony-json-request-transformer! Регистрация слушателя этого события действительно проста. Просто добавьте следующее в ваш 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>

Измените приложение для отправки содержимого файла в формате JSON.

  1. Прочитайте содержимое файла в вашем приложении.
  2. Base64 кодирует содержимое файла
  3. Создать JSON со всем вашим полем (в том числе с содержанием файла)
  4. Отправить JSON на сервер.
  5. Обрабатывать это стандартным способом.

Вы получаете содержимое файла в кодированной строке base64. Затем вы можете расшифровать и проверить его.

Ваш JSON будет выглядеть так:

{
    name: 'Foo',
    phone: '123.345.678',
    profile_image: 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw=='
}
Другие вопросы по тегам