I create the api rest server with Symfony and these bundles FosRestBundle, jms/serializer-bundle, lexik/jwt-authentication-bundle.
How can I send a clean json response format like this :
Missing field "NotNullConstraintViolationException"
{'status':'error','message':"Column 'name' cannot be null"}
or
{'status':'error','message':"Column 'email' cannot be null"}
Or Duplicate entry "UniqueConstraintViolationException" :
{'status':'error','message':"The email [email protected] exists in database."}
Instead of system message:
UniqueConstraintViolationException in AbstractMySQLDriver.php line 66: An exception occurred while executing 'INSERT INTO user (email, name, role, password, is_active) VALUES (?, ?, ?, ?, ?)' with params ["[email protected]", "etienne", "ROLE_USER", "$2y$13$tYW8AKQeDYYWvhmsQyfeme5VJqPsll\/7kck6EfI5v.wYmkaq1xynS", 1]: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '[email protected]' for key 'UNIQ_8D93D649E7927C74'
Return a clean json reponse with the name mandatory or missed field.
Here my controller:
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\User;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\HttpFoundation\JsonResponse;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour toutes les annotations
class DefaultController extends Controller
{
/**
* @Rest\View()
* @Rest\Post("/register")
*/
public function registerAction(Request $request)
{
//catch all errors and convert them to exceptions
ErrorHandler::register();
$em = $this->get('doctrine')->getManager();
$encoder = $this->container->get('security.password_encoder');
$username = $request->request->get('email');
$password = $request->request->get('password');
$name = $request->request->get('name');
$user = new User($username);
$user->setPassword($encoder->encodePassword($user, $password));
$user->setName($name);
$user->setRole('ROLE_USER');
try {
$em->persist($user);
$em->flush($user);
}
catch (NotNullConstraintViolationException $e) {
// Found the name of missed field
return new JsonResponse();
}
catch (UniqueConstraintViolationException $e) {
// Found the name of duplicate field
return new JsonResponse();
}
catch ( \Exception $e ) {
//for debugging you can do like this
$handler = new ExceptionHandler();
$handler->handle( $e );
return new JsonResponse(
array(
'status' => 'errorException',
'message' => $e->getMessage()
)
);
}
return new Response(sprintf('User %s successfully created', $user->getUsername()));
}
}
thanks
We use the following approach:
Generic class for API exceptions:
class ApiException extends \Exception
{
public function getErrorDetails()
{
return [
'code' => $this->getCode() ?: 999,
'message' => $this->getMessage()?:'API Exception',
];
}
}
Create a validation exception extending ApiException
class ValidationException extends ApiException
{
private $form;
public function __construct(FormInterface $form)
{
$this->form = $form;
}
public function getErrorDetails()
{
return [
'code' => 1,
'message' => 'Validation Error',
'validation_errors' => $this->getFormErrors($this->form),
];
}
private function getFormErrors(FormInterface $form)
{
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $childForm) {
if ($childForm instanceof FormInterface) {
if ($childErrors = $this->getFormErrors($childForm)) {
$errors[$childForm->getName()] = $childErrors;
}
}
}
return $errors;
}
}
Use the exception in your controller when the form has errors
if ($form->getErrors(true)->count()) {
throw new ValidationException($form);
}
Create and configure your ExceptionController
class ExceptionController extends FOSRestController
{
public function showAction($exception)
{
$originException = $exception;
if (!$exception instanceof ApiException && !$exception instanceof HttpException) {
$exception = new HttpException($this->getStatusCode($exception), $this->getStatusText($exception));
}
if ($exception instanceof HttpException) {
$exception = new ApiException($this->getStatusText($exception), $this->getStatusCode($exception));
}
$error = $exception->getErrorDetails();
if ($this->isDebugMode()) {
$error['exception'] = FlattenException::create($originException);
}
$code = $this->getStatusCode($originException);
return $this->view(['error' => $error], $code, ['X-Status-Code' => $code]);
}
protected function getStatusCode(\Exception $exception)
{
// If matched
if ($statusCode = $this->get('fos_rest.exception.codes_map')->resolveException($exception)) {
return $statusCode;
}
// Otherwise, default
if ($exception instanceof HttpExceptionInterface) {
return $exception->getStatusCode();
}
return 500;
}
protected function getStatusText(\Exception $exception, $default = 'Internal Server Error')
{
$code = $this->getStatusCode($exception);
return array_key_exists($code, Response::$statusTexts) ? Response::$statusTexts[$code] : $default;
}
public function isDebugMode()
{
return $this->getParameter('kernel.debug');
}
}
config.yml
fos_rest:
#...
exception:
enabled: true
exception_controller: 'SomeBundle\Controller\ExceptionController::showAction'
see: http://symfony.com/doc/current/bundles/FOSRestBundle/4-exception-controller-support.html
With this approach can create custom exceptions with custom messages and codes for each type of error (helpful for API documentation) in the other hand hide other internal exceptions showing to the API consumer only "Internal Server Error" when the exception thrown does not extended from APIException.