使用 Dingo API 扩展包快速构建 Laravel RESTful API(七)—— 错误及异常处理

错误响应

在 Dingo API 中,你可以使用响应构建器支持的多种错误方法生成错误响应:

// 通用的错误响应,包含错误消息和状态码
return $this->response->error('404 Not Found', 404);

// 返回 404 Not Found 响应,等同于上述返回 404 状态码的错误响应
return $this->response->errorNotFound();

// 返回 401 Bad Request 响应,等同于调用 error('Bad Request', 401) 
return $this->response->errorBadRequest();

// 返回 403 Forbidden 响应,等同于调用 error('Forbidden', 403)
return $this->response->errorForbidden();

// 返回 500 Internal Error 响应,等同于调用 error('Internal Error', 500)
return $this->response->errorInternal();

// 返回 401 Unauthorized 响应,等同于调用 error('Unauthorized', 401)
return $this->response->errorUnauthorized();

// 返回 405 Method Not Allowed 响应,比如在 GET 路由上发起 POST 请求就会报这个错
return $this->response->errorMethodNotAllowed();

异常处理

异常响应

在构建 API 的时候处理异常是一件痛苦的事,在 Dingo API 中,你不需要手动构建异常响应,只需要抛出一个继承自 Symfony\Component\HttpKernel\Exception\HttpException 的异常,Dingo API 就会自动为你处理这个响应。

下面是 Dingo API 内置的 Symfony 异常:

异常 状态码
Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException 403
Symfony\Component\HttpKernel\Exception\BadRequestHttpException 400
Symfony\Component\HttpKernel\Exception\ConflictHttpException 409
Symfony\Component\HttpKernel\Exception\GoneHttpException 410
Symfony\Component\HttpKernel\Exception\HttpException 500
Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException 411
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException 405
Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException 406
Symfony\Component\HttpKernel\Exception\NotFoundHttpException 404
Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException 412
Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException 428
Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException 503
Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException 429
Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException 401
Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException 415

下面是一个示例,当我们尝试更新一条已经被别人更新过的记录时抛出一个 ConflictHttpException 异常:

$api->version('v1', function ($api) {
    $api->put('task/{id}', function ($id) {
        $task = \App\Task::find($id);

        if ($task->updated_at > app('request')->get('last_updated')) {
            throw new \Symfony\Component\HttpKernel\Exception\ConflictHttpException('Task was updated prior to your request.');
        }

        // No error, we can continue to update the user as per usual.
    });
});

我们在 Postman 中模拟发起这个 PUT 请求,并设置 last_updated 字段为小于指定任务最后更新时间的值,Dingo API 就会返回一个错误响应:

Dingo 错误响应

Dingo API 会自动捕获抛出的异常并将其转化为 JSON 格式,响应的 HTTP 状态码也会相应更改以匹配这个异常,ConflictHttpException 对应的 HTTP 状态码是 409。

资源异常

以下是资源异常,每个异常都会返回 422 状态码:

Dingo\Api\Exception\DeleteResourceFailedException
Dingo\Api\Exception\ResourceException
Dingo\Api\Exception\StoreResourceFailedException
Dingo\Api\Exception\UpdateResourceFailedException

这些异常的特殊之处在于你可以将创建、更新或者删除资源时遇到的验证错误传递到这些异常中。

下面我们就来看一个创建新用户验证失败抛出 StoreResourceFailedException 异常的例子:

$api->version('v1', function ($api) {
    $api->post('tasks', function () {
        $rules = [
            'text' => ['required', 'string'],
            'is_completed' => ['required', 'boolean']
        ];

        $payload = app('request')->only('text', 'is_completed');

        $validator = app('validator')->make($payload, $rules);

        if ($validator->fails()) {
            throw new \Dingo\Api\Exception\StoreResourceFailedException('Could not create new task.', $validator->errors());
        }

        // Create user as per usual.
    });
});

Dingo API 会自动捕获抛出的异常并将其转化为 JSON 格式,响应的 HTTP 状态码也会更改为与异常相匹配的值,资源异常会返回 422 状态码以及如下 JSON 格式错误信息:

Dingo 资源异常

自定义 HTTP 异常

除了 Dingo 内置支持的异常类之外,你可以创建自定义的 HTTP 异常,前提是它们继承自Symfony\Component\HttpKernel\Exception\HttpException 基类或者实现了Symfony\Component\HttpKernel\Exception\HttpExceptionInterface 接口。

自定义异常响应

如果你需要自定义异常返回的响应可以注册一个异常处理器(你可以在某个服务提供者如 AppServiceProviderboot 方法中添加这段代码):

app(\Dingo\Api\Exception\Handler::class)->register(function (\Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException $exception) {
    return \Illuminate\Support\Facades\Response::make(['error' => 'Hey, what do you think you are doing!?'], 401);
});

现在如果认证失败(抛出 UnauthorizedHttpException 异常)就会返回如下 JSON 格式信息:

{
    "error": "Hey, what do you think you are doing!?"
}

表单请求类

如果你要使用表单请求类,那么需要继承 Dingo API 表单请求基类 Dingo\Api\Http\FormRequest 或者完全自己实现。表单请求类会检查输入请求是否是针对当前 API 的,如果是的话,当验证失败会抛出 Dingo\Api\Exception\ValidationHttpException 异常。这个异常会被 Dingo API 渲染并返回相应的错误响应。

下面我们在 app/Http/Requests 目录下创建一个继承自 Dingo API 请求基类的表单请求类 CreateTaskRequest,并编写其实现代码如下:

<?php

namespace App\Http\Requests;

use Dingo\Api\Http\FormRequest;

class CreateTaskRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'text' => 'required|string',
            'is_completed' => 'required|boolean'
        ];
    }
}

这样一来,我们就可以在控制器方法中声明这个表单请求类来进行表单字段验证了,编写 Api\TaskControllerstore 方法如下:

public function store(CreateTaskRequest $request)
{
    // 表单验证成功,继续后续处理
    return $this->response->errorUnauthorized();
}

在 Postman 中访问对应的路由,如果验证失败,返回错误响应如下:

Dingo 表单验证

如果你想要完全实现自己的表单请求类,则必须重写 failedValidationfailedAuthorization 方法,这些方法必须抛出上述介绍的其中一种异常而不是 Laravel 框架抛出的 HTTP 异常,感兴趣的同学可以自己去实现下,这里就不再介绍了。