Symfony 3 接口异常处理方法

在创建 API 接口时,需要设计固定的JSON 响应结构,以便所有客户端使用相同的格式,如下所示:

{
  "code": 20002,
  "message": "",
  "data": [],
  "errors": {}
}

每个字段的意义:

  1. code: 自定义的返回状态和浏览器状态不同,这里特指逻辑上的状态
  2. message:成功或失败都可以返回消息内容
  3. data:成功的返回值
  4. errors:错误信息

定义个最简单的接口响应类:

namespace App\Http;
use Symfony\Component\HttpFoundation\JsonResponse;
	
	class ApiResponse extends JsonResponse
	{
	    /**
	     * ApiResponse constructor.
	     *
	     * @param string $message
	     * @param mixed  $data
	     * @param array  $errors
	     * @param int    $status
	     * @param array  $headers
	     * @param bool   $json
	     */
	    public function __construct(string $message, $data = null, array $errors = [], int $status = 200, array $headers = [], bool $json = false)
	    {
	        parent::__construct($this->format($message, $data, $errors), $status, $headers, $json);
	    }
	

	    /**
	     * Format the API response.
	     *
	     * @param string $message
	     * @param mixed  $data
	     * @param array  $errors
	     *
	     * @return array
	     */
	    private function format(string $message, $data = null, array $errors = [])
	    {
	        if ($data === null) {
	            $data = new \ArrayObject();
	        }
	        $response = [
	            'message' => $message,
	            'data'    => $data,
	        ];
	        if ($errors) {
	            $response['errors'] = $errors;
	        }
	        return $response;
	    }
	}

这种方式用作接口返回值时非常游泳,但是当我们同时需要以 api 响应的方式处理异常时,这个方式就不合适了。

接口异常处理

创建一个 EventListener 来处理异常,并通过我们设计的返回格式返回响应结果。

namespace App\EventListener;
	
	use Symfony\Component\HttpFoundation\Response;
	use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
	use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
	
	class ExceptionListener
	{
	    /**
	     * @param GetResponseForExceptionEvent $event
	     */
	    public function onKernelException(GetResponseForExceptionEvent $event)
	    {
	        $exception = $event->getException();
	        $request   = $event->getRequest();
	        if (in_array('application/json', $request->getAcceptableContentTypes())) {
	            $response = $this->createApiResponse($exception);
	            $event->setResponse($response);
	        }
	    }
	    
	    /**
	     * Creates the ApiResponse from any Exception	    
	     * @param \Exception $exception
	     * @return ApiResponse
	     */
	    private function createApiResponse(\Exception $exception)
	    {
	        $statusCode = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR;
	        $errors     = [];
	        return new ApiResponse($exception->getMessage(), null, $errors, $statusCode);
	    }
	}

$errors 是一个空数组。我们现在需要做的是将字段错误,比如表单错误,转换为关联数组。

在这里,Normalizer 组件帮助我们将任何数据,在这种情况下是异常,转化为一个数组。

归一化表单异常 Normalize form exceptions

这时我们要为表单异常创建一个Normalizer,但首先让我们创建一个 FormException。

namespace App\Exception;
	
	use Symfony\Component\Form\FormErrorIterator;
	use Symfony\Component\Form\FormInterface;
	use Symfony\Component\HttpKernel\Exception\HttpException;
	
	class FormException extends HttpException
	{
	    /**
	     * @var FormInterface
	     */
	    protected $form;
	
	    /**
	     * HttpFormException constructor.
	     *
	     * @param FormInterface   $form
	     * @param int             $statusCode
	     * @param string|null     $message
	     * @param \Exception|null $previous
	     * @param array           $headers
	     * @param int|null        $code
	     */
	    public function __construct(FormInterface $form, int $statusCode = 400, string $message = null, \Exception $previous = null, array $headers = [], ?int $code = 0)
	    {
	        parent::__construct($statusCode, $message, $previous, $headers, $code);
	        $this->form = $form;
	    }
	

	    /**
	     * @return FormInterface
	     */
	    public function getForm()
	    {
	        return $this->form;
	    }
	

	    /**
	     * @return FormErrorIterator
	     */
	    public function getErrors()
	    {
	        return $this->form->getErrors(true);
	    }
	}

现在我们有了一个FormException,让我们创建相关的normalizer…

namespace App\Serializer;
	
	use App\Exception\FormException;
	use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
	
	class FormExceptionNormalizer implements NormalizerInterface
	{
	    /**
	     * @param FormException $exception
	     * @param null          $format
	     * @param array         $context
	     *
	     * @return array|bool|float|int|string|void
	     */
	    public function normalize($exception, $format = null, array $context = [])
	    {
	        $data   = [];
	        $errors = $exception->getErrors();
	        foreach ($errors as $error) {
	            $data[$error->getOrigin()->getName()][] = $error->getMessage();
	        }
	        return $data;
	    }
	

	    /**
	     * @param mixed $data
	     * @param null  $format
	     *
	     * @return bool|void
	     */
	    public function supportsNormalization($data, $format = null)
	    {
	        return $data instanceof FormException;
	    }
	}

我们需要在service.yaml文件中把它注册为serializer.normalizer

services:
    App\EventListener\ExceptionListener:
        tags:
	            - { name: kernel.event_listener, event: kernel.exception }
    App\Serializer\FormExceptionNormalizer:
        tags: ['serializer.normalizer']

Normalizer factory

和表单异常一样,我们也可以有其他的自定义字段异常,比如验证对象后的违规列表,所以我们需要创建一个 Factory 来为传递的异常获取正确的 Normalizer

namespace App\Factory;

	use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

	class NormalizerFactory
	{
	    /**
	     * @var NormalizerInterface[]
	     */
	    private $normalizers;

	    /**
	     * NormalizerFactory constructor.
	     *
	     * @param iterable $normalizers
	     */
	    public function __construct(iterable $normalizers)
	    {
	        $this->normalizers = $normalizers;
	    }
	

	    /**
	     * Returns the normalizer by supported data.
	     *
	     * @param mixed $data
	     *
	     * @return NormalizerInterface|null
	     */
	    public function getNormalizer($data)
	    {
	        foreach ($this->normalizers as $normalizer) {
	            if ($normalizer instanceof NormalizerInterface && $normalizer->supportsNormalization($data)) {
	                return $normalizer;
	            }
	        }
	        return null;
	    }
	}

为了将注册的normalizer传递给NormalizerFactory构造方法,Symfony提供了一个快捷方式来注入所有带有特定标签的服务

services:
	    App\EventListener\ExceptionListener:
	        tags:
	            - { name: kernel.event_listener, event: kernel.exception }
	            
	    App\Serializer\FormExceptionNormalizer:
	        tags: ['serializer.normalizer']
	    
	    App\Factory\NormalizerFactory:
	        arguments: [!tagged serializer.normalizer]
	        public: true

现在我们需要做的就是更新ExceptionListerner…

namespace App\EventListener;
	
	use Symfony\Component\HttpFoundation\Response;
	use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
	use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
	
	class ExceptionListener
	{
	    /**
	     * @var NormalizerFactory 
	     */
	    private $normalizerFactory;
	    
	    /**
	     * ExceptionListener constructor.
	     *
	     * @param NormalizerFactory $normalizerFactory
	     */
	    public function __construct(NormalizerFactory $normalizerFactory)
	    {
	        $this->normalizerFactory = $normalizerFactory;
	    }
	    
	    /**
	     * @param GetResponseForExceptionEvent $event
	     */
	    public function onKernelException(GetResponseForExceptionEvent $event)
	    {
	        $exception = $event->getException();
	        $request   = $event->getRequest();
	        if (in_array('application/json', $request->getAcceptableContentTypes())) {
	            $response = $this->createApiResponse($exception);
	            $event->setResponse($response);
	        }
	    }
	    
	    /**
	     * Creates the ApiResponse from any Exception
	     *
	     * @param \Exception $exception
	     *
	     * @return ApiResponse
	     */
	    private function createApiResponse(\Exception $exception)
	    {
	        $normalizer = $this->normalizerFactory->getNormalizer($exception);
	        $statusCode = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR;
	        try {
	            $errors = $normalizer ? $normalizer->normalize($exception) : [];
	        } catch (\Exception $e) {
	            $errors = [];
	        }
	        return new ApiResponse($exception->getMessage(), null, $errors, $statusCode);
	    }
	}

转载地址:Symfony 4 — A good way to deal with exceptions for REST API

发表评论