Handling requests that don't have files attached to them is easy otherwise is tricky. Logic flow of example below is as follows.


Flow



Upload folder


Create src/Application/FrontendBundle/Resources/upload/.gitkeep folder.


Parameters.yml


parameters:
upload_dir: %kernel.root_dir%/../src/Application/FrontendBundle/Resources/upload/

Controllers.yml


services:
application_frontend.controller.abstract:
class: Application\BackendBundle\Controller\AbstractController
abstract: true
arguments:
- @serializer
- @validator
- @doctrine_common_inflector

application_frontend.controller.message:
class: Application\FrontendBundle\Controller\MessageController
parent: application_backend.controller.abstract
arguments:
- @application_frontend.service.message

Services.yml


services:
application_frontend.service.message:
class: Application\FrontendBundle\Service\MessageService
arguments:
- @application_frontend.factory.message
- @application_frontend.util.upload_helper

Factories.yml


services:
application_frontend.factory.message:
class: Application\FrontendBundle\Factory\MessageFactory

Utils.yml


services:
application_frontend.util.upload_helper:
class: Application\FrontendBundle\Util\UploadHelper
arguments:
- %upload_dir%

AbstractController.php


namespace Application\FrontendBundle\Controller;

use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

abstract class AbstractController
{
private $validContentTypes = ['json' => 'application/json', 'xml' => 'application/xml'];

protected $serializer;
protected $validator;
protected $inflector;

public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
$this->serializer = $serializer;
$this->validator = $validator;
$this->inflector = $inflector;
}

/**
* @param string $contentType
*
* @return string|Response
*/
protected function validateContentType($contentType)
{
if (!in_array($contentType, $this->validContentTypes)) {
return $this->createFailureResponse(
['content_type' => sprintf('Invalid content type [%s].', $contentType)],
'json',
415
);
}

return array_search($contentType, $this->validContentTypes);
}

/**
* @param string $payload
* @param string $model
* @param string $format
*
* @return object|Response
*/
protected function validatePayload($payload, $model, $format)
{
$payload = $this->serializer->deserialize($payload, $model, $format);

$errors = $this->validator->validate($payload);
if (count($errors)) {
return $this->createFailureResponse($errors, $format);
}

return $payload;
}

/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createSuccessResponse($content, $format = 'json', $status = 200)
{
return $this->getResponse($content, $format, $status);
}

/**
* @param array|ConstraintViolationListInterface $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createFailureResponse($content, $format = 'json', $status = 400)
{
$errorList = null;

if ($content instanceof ConstraintViolationList) {
foreach ($content as $error) {
$error = $this->getErrorFromValidation($error);
$errorList[$error['key']] = $error['value'];
}
} else {
$errorList = $content;
}

return $this->getResponse(['errors' => $errorList], $format, $status);
}

/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
private function getResponse($content, $format, $status)
{
$context = new SerializationContext();
$context->setSerializeNull(false);

$response = $this->serializer->serialize($content, $format, $context);

return new Response($response, $status, ['Content-Type' => $this->validContentTypes[$format]]);
}

/**
* @param ConstraintViolationInterface $error
*
* @return array
*/
private function getErrorFromValidation($error)
{
$properties = $this->inflector->tableize($error->getPropertyPath());

return ['key' => $properties, 'value' => $error->getMessage()];
}
}

MessageController.php


namespace Application\FrontendBundle\Controller;

use Application\FrontendBundle\Service\MessageServiceInterface;
use Doctrine\Common\Inflector\Inflector;
use Exception;
use JMS\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @Route("message", service="application_frontend.controller.message")
*/
class MessageController extends AbstractController
{
private $messageService;

public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector,
MessageServiceInterface $messageService
) {
parent::__construct($serializer, $validator, $inflector);

$this->messageService = $messageService;
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("")
*
* @return Response
*/
public function messageAction(Request $request)
{
try {
// Get files and the request payload
$files = $request->files->all();
$payload = json_decode($request->getContent(), true);

// Create a model from the files and the request payload
$message = $this->messageService->createMessageModel(
isset($payload['body']) ? $payload['body'] : null,
$files
);

// Validate model
$errors = $this->validator->validate($message);
if (count($errors)) {
return $this->createFailureResponse($errors);
}

$result = $this->messageService->upload($message);

print_r($result);

return $this->createSuccessResponse('Successful');
} catch (Exception $e) {
throw new UploadException($e->getMessage());
}
}
}

MessageServiceInterface.php


namespace Application\FrontendBundle\Service;

use Application\FrontendBundle\Model\Message;
use Application\FrontendBundle\Model\Result;
use Application\FrontendBundle\Model\Upload;

interface MessageServiceInterface
{
/**
* @param null|string $body
* @param array $files
*
* @return Upload
*/
public function createMessageModel($body = null, array $files = []);

/**
* @param Message $message
*
* @return Result
*/
public function upload(Message $message);
}

MessageService.php


namespace Application\FrontendBundle\Service;

use Application\FrontendBundle\Factory\MessageFactoryInterface;
use Application\FrontendBundle\Model\Message;
use Application\FrontendBundle\Model\Result;
use Application\FrontendBundle\Util\UploadHelper;

class MessageService implements MessageServiceInterface
{
private $messageFactory;
private $uploadHelper;

public function __construct(
MessageFactoryInterface $messageFactory,
UploadHelper $uploadHelper
) {
$this->messageFactory = $messageFactory;
$this->uploadHelper = $uploadHelper;
}

/**
* @param null|string $body
* @param array $files
*
* @return Message
*/
public function createMessageModel($body = null, array $files = [])
{
return $this->messageFactory->createMessageModel($body, $files);
}

/**
* @param Message $message
*
* @return Result
*/
public function upload(Message $message)
{
return $this->uploadHelper->attachments($message);
}
}

MessageFactoryInterface.php


namespace Application\FrontendBundle\Factory;

use Application\FrontendBundle\Model\Message;

interface MessageFactoryInterface
{
/**
* @param null|string $body
* @param array $files
*
* @return Message
*/
public function createMessageModel($body = null, array $files = []);
}

MessageFactory.php


namespace Application\FrontendBundle\Factory;

use Application\FrontendBundle\Model\File;
use Application\FrontendBundle\Model\Message;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class MessageFactory implements MessageFactoryInterface
{
/**
* @param null|string $body
* @param array $files
*
* @return Message
*/
public function createMessageModel($body = null, array $files = [])
{
$message = new Message();
$message->body = $body;

/** @var UploadedFile $uploadedFile */
foreach ($files as $uploadedFile) {
$file = new File();
$file->file = $uploadedFile;

$message->files[] = $file;
}

return $message;
}
}

UploadHelper.php


namespace Application\FrontendBundle\Util;

use Application\FrontendBundle\Model\File;
use Application\FrontendBundle\Model\Message;
use Application\FrontendBundle\Model\Result;
use Application\FrontendBundle\Model\Upload;
use Exception;
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class UploadHelper
{
private $uploadDir;

public function __construct($uploadDir)
{
$this->uploadDir = $uploadDir;
}

/**
* @param Message $message
*
* @return Result
*
* @throws UploadException
*/
public function attachments(Message $message)
{
try {
$result = new Result();
$result->message = $message->body;

/** @var File $file */
foreach ($message->files as $files) {
$filename = sprintf(
'%s.%s',
$this->createFilename($files->file),
pathinfo($files->file->getClientOriginalName(), PATHINFO_EXTENSION)
);
$files->file->move($this->uploadDir, $filename);

$upload = new Upload();
$upload->name = $files->file->getClientOriginalName();
$upload->path = $this->uploadDir.$filename;
$upload->mime = $files->file->getClientMimeType();

$result->uploads[] = $upload;
}

return $result;
} catch (Exception $e) {
throw new UploadException($e->getMessage());
}
}

/**
* @param UploadedFile $uploadedFile
*
* @return string
*/
private function createFilename(UploadedFile $uploadedFile)
{
return sha1($uploadedFile->getFilename().$uploadedFile->getSize().microtime().mt_rand());
}
}

Message.php


namespace Application\FrontendBundle\Model;

use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;

class Message
{
/**
* @var string
*
* @Assert\NotBlank(message="You must supply a message body.")
*
* @Serializer\Type("string")
*/
public $body;

/**
* @var File[]
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("array<Application\FrontendBundle\Model\File>")
*/
public $files = [];
}

File.php


namespace Application\FrontendBundle\Model;

use Symfony\Component\HttpFoundation\File\File as UploadedFile;

class File
{
/**
* @var UploadedFile
*/
public $file;
}

You can add mime validation types like below as well.


use Symfony\Component\Validator\Constraints as Assert;

/**
* @Assert\File(
* mimeTypes = {
* "image/png",
* "image/gif",
* "image/jpg",
* "image/jpeg"
* }
* )
*/

Result.php


namespace Application\FrontendBundle\Model;

class Result
{
/**
* @var string
*/
public $message;

/**
* @var Upload[]
*/
public $uploads = [];
}

Upload.php


namespace Application\FrontendBundle\Model;

class Upload
{
/**
* @var string
*/
public $name;

/**
* @var string
*/
public $path;

/**
* @var string
*/
public $mime;
}

Test without attachments


Endpoint POST http://football.local/app_dev.php/message


{
"body": "Hello"
}

Response


Application\FrontendBundle\Model\Result Object
(
[message] => Hello
[uploads] => Array
(
)
)
"Successful"

Test with attachments


I simulated this version with a Behat test so the files are attached to the request on-the-fly in FeatureContext file if you know what I'm talking about! You can find an example about how it is done in Behat posts of this blog.


{
"body": "I also have attachments in this request"
}

Response


Application\FrontendBundle\Model\Result Object
(
[message] => I also have attachments in this request
[uploads] => Array
(
[0] => Application\FrontendBundle\Model\Upload Object
(
[name] => dummy.png
[path] => /Library/WebServer/Documents/football/app/../src/Application/FrontendBundle/Resources/upload/361da8a6da4c7bc23bf36f0ffb26318c5f2cbbf3.png
[mime] => image/png
)
[1] => Application\FrontendBundle\Model\Upload Object
(
[name] => dummy.doc
[path] => /Library/WebServer/Documents/football/app/../src/Application/FrontendBundle/Resources/upload/dc35300bfaf6e5c686c3ac31cb38ee55745c0109.doc
[mime] => application/msword
)
)
)