3

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

4
  • Are you sure this code is properly executed? I just tried it in an existing symfony project and the catch() works as expected Commented May 8, 2017 at 16:14
  • The code is work, but i want custom message error for the json format. Commented May 8, 2017 at 16:16
  • Ah, sorry, misread the question. So you want to return the name of the missed field or duplicate entry? Does the answer by rafrsr solve this (partially)? Commented May 8, 2017 at 16:20
  • Just partially, because in my controller i dont use a form. Commented May 8, 2017 at 16:46

1 Answer 1

10

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.