{
"success": true,
"code": 0,
"message": "ok",
"data": [
{
"id": 1,
"name": "Ada"
}
],
"meta": {
"pagination": "page",
"page": 1,
"page_size": 20,
"has_more": false,
"total": 1
}
}- 支持成功、失败和认证失败三种响应
- 支持页码分页和游标分页
- 几乎零改动即可支持 API Resource
- 支持自定义响应头
- 支持异常返回
- PHP ^8.2
- Laravel ^11.0 | ^12.0 | ^13.0
通过 Composer 安装:
composer require mitoop/laravel-api-response安装后即可使用。默认值如下:
| 配置 | 默认值 |
|---|---|
成功 success / 失败 error / 认证失败 deny |
业务码 0 / 1 / -1 |
message.success / message.error / message.deny |
ok / error / unauthorized |
http_status.success / http_status.error / http_status.deny |
HTTP 状态 200 / 200 / 200 |
要改默认值,在 AppServiceProvider 的 boot 里调用 apply。没传入的项保持默认值:
use Illuminate\Support\Str;
use Mitoop\Http\ResponderConfig;
public function boot(): void
{
app(ResponderConfig::class)->apply([
'success' => 0,
'error' => 1,
'deny' => -1,
'message' => [
'success' => 'ok',
'error' => 'error',
'deny' => 'unauthorized',
],
'http_status' => [
'success' => 200,
'error' => 200,
'deny' => 200,
],
'extra' => [
'request_id' => fn () => (string) Str::uuid7(),
],
]);
}extra 合并到每次响应的最外层。值可以是闭包,生成响应时才执行。上面的 request_id 由应用自己生成。某次响应已经带了同名字段时,对应闭包不会执行。
success、code、message、data、meta、errors 是保留字段。extra 里的同名字段会被忽略。
成功响应:
{
"success": true,
"code": 0,
"message": "ok",
"data": {
"id": 1,
"name": "Ada"
}
}data 为空时是 JSON 对象 {},列表是数组。业务码在 code。HTTP 状态单独设置,默认都是 200。
分页信息在 meta,与 data 同级。
页码分页:
{
"pagination": "page",
"page": 1,
"page_size": 20,
"has_more": false,
"total": 1
}simplePaginate 不带 total。
游标分页:
{
"pagination": "cursor",
"page_size": 20,
"has_more": true,
"next_cursor": "eyJpZCI6MX0"
}没有下一页时,next_cursor 为 null。
失败响应:
{
"success": false,
"code": 1,
"message": "名称必填",
"data": {},
"errors": {
"name": ["名称必填"]
}
}认证失败响应:
{
"success": false,
"code": -1,
"message": "登录信息已失效,请重新登录",
"data": {},
"errors": {}
}业务码默认是 -1。没有字段错误时,errors 是空对象 {}。
在控制器中引入 RespondsWithJson。success 方法返回成功响应,error 方法返回失败响应,deny 方法返回认证失败响应:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Mitoop\Http\RespondsWithJson;
class UserController extends Controller
{
use RespondsWithJson;
public function index()
{
return $this->success(User::query()->paginate());
}
public function store()
{
return $this->error('名称必填', errors: [
'name' => ['名称必填'],
], status: 422);
}
public function destroy()
{
return $this->deny('登录信息已失效,请重新登录');
}
}status 只设置 HTTP 状态,code 仍用配置里的业务码。不返回数据时直接调用 success:
return $this->success();游标分页把 paginate 换成 cursorPaginate:
return $this->success(User::query()->cursorPaginate());资源类只要改成继承本包的基类即可。使用方式不用变,还是直接返回资源,响应就会按统一结构输出。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Mitoop\Http\Resources\Resource;
class UserResource extends Resource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}<?php
namespace App\Http\Resources;
use Mitoop\Http\Resources\ResourceCollection;
class UserCollection extends ResourceCollection
{
//
}public function show(User $user)
{
return new UserResource($user);
}
public function index()
{
return new UserCollection(User::query()->paginate());
}如果想更改本次响应的文案或 HTTP 状态,可以链式调用 withMessage 和 withStatus。单条资源和集合都支持:
return (new UserResource($user))
->withMessage('创建成功')
->withStatus(201);
return (new UserCollection(User::query()->paginate()))
->withMessage('获取成功');发布资源模板后,make:resource 会按本包的继承关系自动生成。执行 php artisan stub:publish,将 stubs/resource.stub 的父类改为 Mitoop\Http\Resources\Resource,将 stubs/resource-collection.stub 的父类改为 Mitoop\Http\Resources\ResourceCollection。
默认不加自定义响应头。需要附加响应头时,实现 HeaderResolverInterface,并在服务提供者的 register 里替换默认绑定,例如给响应报文加签名。控制器响应和 API Resource 都会带上这些头。
用参数里的 $jsonOptions 编码 payload,结果和响应体一致:
<?php
namespace App\Http;
use Mitoop\Http\Headers\HeaderResolverInterface;
class SignatureHeader implements HeaderResolverInterface
{
/**
* @param array<string, mixed> $payload
* @return array<string, string>
*/
public function resolve(array $payload, int $jsonOptions): array
{
return [
'X-Signature' => hash('sha256', json_encode($payload, $jsonOptions)),
];
}
}use App\Http\SignatureHeader;
use Mitoop\Http\Headers\HeaderResolverInterface;
public function register(): void
{
$this->app->singleton(HeaderResolverInterface::class, SignatureHeader::class);
}框架自己的异常处理不变。接口异常也要这套 JSON 时,可以加一个 ApiExceptionRenderer:
<?php
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
use Mitoop\Http\Responder;
use stdClass;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class ApiExceptionRenderer
{
public function __construct(private Responder $responder) {}
public function __invoke(Throwable $e, Request $request): JsonResponse|false
{
if (! $request->expectsJson()) {
return false;
}
return match (true) {
$e instanceof AuthenticationException => $this->responder->deny('请先登录'),
$e instanceof ValidationException => $this->responder->error(
(string) Arr::first(Arr::flatten($e->errors())),
errors: $e->errors(),
),
$e instanceof NotFoundHttpException => $this->responder->error('没有找到对应内容'),
default => $this->responder->error(
'服务暂时不可用,请稍后再试',
errors: app()->hasDebugModeEnabled() ? [
'exception' => $e::class,
'message' => $e->getMessage(),
] : new stdClass,
),
};
}
}不是 JSON 请求时返回 false,Laravel 会继续使用原来的异常页面或重定向。也可以去掉这个判断,让所有请求都通过统一结构返回。按需要选择。
再到 bootstrap/app.php 注册。业务异常按同样方式加进 match:
use App\Exceptions\ApiExceptionRenderer;
use Illuminate\Http\Request;
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(fn (Throwable $e, Request $request) => app(ApiExceptionRenderer::class)($e, $request));
});MIT