Symfony 3 接口异常处理方法转载

July 25, 2020
haiqing
52
从更高的层级定义出接口的返回格式和异常处理

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


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


每个字段的意义:

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


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


<?php
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 来处理异常,并通过我们设计的返回格式返回响应结果。


<?php
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。

<?php
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...


<?php
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

<?php
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...

<?php
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