In this example we are going to create custom exceptions and an event listener to return standard response to end user. There will be just two examples but you can extend it by adding more exceptions. No need to change any other code at all.


Response body


The reason why I use messages as an array but not message as single string is because some validations might generate more than one reason. You'll see what I mean in tests below.


{
"error": {
"code": {An inreger error code goes here which matches header status code},
"messages": [{Zero or more error messages}]
}
}

Controller


The way you can use custom exceptions are listed below.


namespace Inanzzz\ApplicationBundle\Controller;

use Inanzzz\ApplicationBundle\Exception\BadRequestException;
use Inanzzz\ApplicationBundle\Exception\NotFoundException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("", service="inanzzz_application.controller.default")
*/
class DefaultController
{
/**
* @Method({"GET"})
* @Route("/not-found-exception")
*
* @return Response
*
* @throws NotFoundException
*/
public function notFoundExceptionAction()
{
throw new NotFoundException();
throw new NotFoundException('Order you are looking for cannot be found.');
}

/**
* @Method({"GET"})
* @Route("/bad-request-exception")
*
* @return Response
*
* @throws BadRequestException
*/
public function badRequestExceptionAction()
{
throw new BadRequestException();
throw new BadRequestException([]);
throw new BadRequestException(['Name field is required.']);
throw new BadRequestException(['Name field is required.', 'Postcode field is required.']);
}
}

services:
inanzzz_application.controller.default:
class: Inanzzz\ApplicationBundle\Controller\DefaultController

Exceptions


ApiExceptionInterface


namespace Inanzzz\ApplicationBundle\Exception;

interface ApiExceptionInterface
{
}

NotFoundException


Accepts only string type error message or nothing at all. It throws 404 Not Found exception.


namespace Inanzzz\ApplicationBundle\Exception;

use Exception;
use Symfony\Component\HttpFoundation\Response;

class NotFoundException extends Exception implements ApiExceptionInterface
{
public function __construct($message = null)
{
parent::__construct($message, Response::HTTP_NOT_FOUND);
}
}

BadRequestException


Accepts only array type error message or nothing at all. It throws 400 Bad Request exception.


namespace Inanzzz\ApplicationBundle\Exception;

use Exception;
use Symfony\Component\HttpFoundation\Response;

class BadRequestException extends Exception implements ApiExceptionInterface
{
public function __construct(
array $message = [],
$code = Response::HTTP_BAD_REQUEST,
Exception $previous = null
) {
parent::__construct(json_encode($message), $code, $previous);
}
}

ApiExceptionListener


namespace Inanzzz\ApplicationBundle\EventListener;

use Inanzzz\ApplicationBundle\Exception\ApiExceptionInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

class ApiExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
if (!$event->getException() instanceof ApiExceptionInterface) {
return;
}

$response = new JsonResponse($this->buildResponseData($event->getException()));
$response->setStatusCode($event->getException()->getCode());

$event->setResponse($response);
}

private function buildResponseData(ApiExceptionInterface $exception)
{
$messages = json_decode($exception->getMessage());
if (!is_array($messages)) {
$messages = $exception->getMessage() ? [$exception->getMessage()] : [];
}

return [
'error' => [
'code' => $exception->getCode(),
'messages' => $messages
]];
}
}

services:

inanzzz_application.listener.api_exception:
class: Inanzzz\ApplicationBundle\EventListener\ApiExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

Test


404 Not Found


Calling GET /not-found-exception endpoint.


# 1
{
"error": {
"code": 404,
"messages": []
}
}

# 2
{
"error": {
"code": 404,
"messages": [
"Order you are looking for cannot be found."
]
}
}

400 Bad Request


Calling GET /bad-request-exception endpoint.


# 1
{
"error": {
"code": 400,
"messages": []
}
}

# 2
{
"error": {
"code": 400,
"messages": []
}
}

# 3
{
"error": {
"code": 400,
"messages": [
"Name field is required."
]
}
}

# 4
{
"error": {
"code": 400,
"messages": [
"Name field is required.",
"Postcode field is required."
]
}
}