Write the Code. Change the World.

10月 23

响应格式的统一,对前端来说,是一种更好的体验。这里借鉴别人的地方,仅仅对 response 做统一格式响应。

规范的响应结构

RESTful 服务最佳实践

code—— 包含一个整数类型的 HTTP 响应状态码。
status—— 包含文本:”success”,”fail” 或”error”。HTTP 状态响应码在 500-599 之间为”fail”,在 400-499 之间为”error”,其它均为”success”(例如:响应状态码为 1XX、2XX 和 3XX)。
message—— 当状态值为”fail” 和”error” 时有效,用于显示错误信息。参照国际化(il8n)标准,它可以包含信息号或者编码,可以只包含其中一个,或者同时包含并用分隔符隔开。
data—— 包含响应的 body。当状态值为”fail” 或”error” 时,data 仅包含错误原因或异常名称。

说明

整体响应结构设计参考如上,相对严格地遵守了 RESTful 设计准则,返回合理的 HTTP 状态码。

考虑到业务通常需要返回不同的 “业务描述处理结果”,在所有响应结构中都支持传入符合业务场景的 message。

整体响应结构设计参考如上,相对严格地遵守了 RESTful 设计准则,返回合理的 HTTP 状态码。

考虑到业务通常需要返回不同的“业务描述处理结果”,在所有响应结构中都支持传入符合业务场景的message

  • data:
    • 查询单条数据时直接返回对象结构,减少数据层级;
    • 查询列表数据时返回数组结构;
    • 创建或更新成功,返回修改后的数据;(也可以不返回数据直接返回空对象)
    • 删除成功时返回空对象
  • status:
    • error, 客户端(前端)出错,HTTP 状态响应码在400-599之间。如,传入错误参数,访问不存在的数据资源等
    • fail,服务端(后端)出错,HTTP 状态响应码在500-599之间。如,代码语法错误,空对象调用函数,连接数据库失败,undefined index等
    • success, HTTP 响应状态码为1XX、2XX和3XX,用来表示业务处理成功。
  • message: 描述执行的请求操作处理的结果;也可以支持国际化,根据实际业务需求来切换。
  • code: HTTP 响应状态码;可以根据实际业务需求,调整成业务操作码

操作一波

新建 app/Support/Traits/ResponseTrait

<?php

/*
 * This file is part of the Jiannei/lumen-api-starter.
 *
 * (c) Jiannei <longjian.huang@foxmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace App\Support\Traits;

use App\Support\Response;
use Illuminate\Http\Request;
use Throwable;

/**
 * Trait Helpers.
 *
 * @property \App\Support\Response $response
 */
trait ResponseTrait
{
    public function __get($key)
    {
        $callable = [
            'response',
        ];

        if (in_array($key, $callable) && method_exists($this, $key)) {
            return $this->$key();
        }

        throw new \ErrorException('Undefined property '.get_class($this).'::'.$key);
    }

    /**
     * @return Response
     */
    protected function response()
    {
        return app(Response::class);
    }

    /**
     * Custom Normal Exception response.
     *
     * @param $request
     * @param  Throwable  $e
     */
    protected function prepareJsonResponse($request, Throwable $e)
    {
        // 要求请求头 header 中包含 /json 或 +json,如:Accept:application/json
        // 或者是 ajax 请求,header 中包含 X-Requested-With:XMLHttpRequest;
        $this->response->fail(
            '',
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
            $this->convertExceptionToArray($e),
            $this->isHttpException($e) ? $e->getHeaders() : [],
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
        );
    }

    /**
     * Custom Failed Validation Response.
     *
     * @param Request $request
     * @param array   $errors
     *
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    protected function buildFailedValidationResponse(Request $request, array $errors)
    {
        if (isset(static::$responseBuilder)) {
            return call_user_func(static::$responseBuilder, $request, $errors);
        }

        $this->response->fail('Validation error', 422, $errors);
    }
}

再建 App/Support/Response

<?php

/*
 * This file is part of the Jiannei/lumen-api-starter.
 *
 * (c) Jiannei <longjian.huang@foxmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace App\Support;

use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response as HttpResponse;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\HigherOrderTapProxy;

class Response
{
    public function accepted(string $message = '')
    {
        return $this->success(null, $message, HttpResponse::HTTP_ACCEPTED);
    }

    public function created($data = null, string $message = '', string $location = '')
    {
        $response = $this->success($data, $message, HttpResponse::HTTP_CREATED);
        if ($location) {
            $response->header('Location', $location);
        }

        return $response;
    }

    public function errorBadRequest(?string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_BAD_REQUEST);
    }

    public function errorForbidden(string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_FORBIDDEN);
    }

    public function errorInternal(string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_INTERNAL_SERVER_ERROR);
    }

    public function errorMethodNotAllowed(string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
    }

    public function errorNotFound(string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_NOT_FOUND);
    }

    public function errorUnauthorized(string $message = '')
    {
        $this->fail($message, HttpResponse::HTTP_UNAUTHORIZED);
    }

    /**
     * @param  string  $message
     * @param  int  $code
     * @param  null  $data
     * @param  array  $header
     * @param  int  $options
     *
     * @throws HttpResponseException
     */
    public function fail(string $message = '', int $code = HttpResponse::HTTP_INTERNAL_SERVER_ERROR, $data = null, array $header = [], int $options = 0)
    {
        response()->json(
            $this->formatData($data, $message, $code),
            $code,
            $header,
            $options
        )->throwResponse();
    }

    public function noContent(string $message = '')
    {
        return $this->success(null, $message, HttpResponse::HTTP_NO_CONTENT);
    }

    /**
     * @param  JsonResource|array|null  $data
     * @param  string  $message
     * @param  int  $code
     * @param  array  $headers
     * @param  int  $option
     *
     * @return JsonResponse|JsonResource
     */
    public function success($data = null, string $message = '', $code = HttpResponse::HTTP_OK, array $headers = [], $option = 0)
    {
        if (! $data instanceof JsonResource) {
            return $this->formatArrayResponse($data, $message, $code, $headers, $option);
        }

        if ($data instanceof ResourceCollection && ($data->resource instanceof AbstractPaginator)) {
            return $this->formatPaginatedResourceResponse(...func_get_args());
        }

        return $this->formatResourceResponse(...func_get_args());
    }

    /**
     * Format normal array data.
     *
     * @param  array|null  $data
     * @param  string  $message
     * @param  int  $code
     * @param  array  $headers
     * @param  int  $option
     *
     * @return JsonResponse
     */
    protected function formatArrayResponse($data, string $message = '', $code = HttpResponse::HTTP_OK, array $headers = [], $option = 0)
    {
        return response()->json($this->formatData($data, $message, $code), $code, $headers, $option);
    }

    /**
     * @param  JsonResource|array|null  $data
     * @param $message
     * @param $code
     *
     * @return array
     */
    protected function formatData($data, $message, &$code)
    {
        $originalCode = $code;
        $code = (int) substr($code, 0, 3); // notice

        if ($code >= 400 && $code <= 499) {// client error
            $status = 'error';
        } elseif ($code >= 500 && $code <= 599) {// service error
            $status = 'fail';
        } else {
            $status = 'success';
        }

        return [
            'status' => $status,
            'code' => $originalCode,
            'message' => $message,
            'data' => $data ?: (object) $data,
        ];
    }

    /**
     * Format paginated resource data.
     *
     * @param  JsonResource  $resource
     * @param  string  $message
     * @param  int  $code
     * @param  array  $headers
     * @param  int  $option
     *
     * @return HigherOrderTapProxy|mixed
     */
    protected function formatPaginatedResourceResponse($resource, string $message = '', $code = HttpResponse::HTTP_OK, array $headers = [], $option = 0)
    {
        $paginated = $resource->resource->toArray();

        $paginationInformation = [
            'meta' => [
                'pagination' => [
                    'total' => $paginated['total'] ?? null,
                    'count' => $paginated['to'] ?? null,
                    'per_page' => $paginated['per_page'] ?? null,
                    'current_page' => $paginated['current_page'] ?? null,
                    'total_pages' => $paginated['last_page'] ?? null,
                    'links' => [
                        'previous' => $paginated['prev'] ?? null,
                        'next' => $paginated['next_page_url'] ?? null,
                    ],
                ],
            ],
        ];

        $data = array_merge_recursive(['data' => $this->parseDataFrom($resource)], $paginationInformation);

        return tap(
            response()->json($this->formatData($data, $message, $code), $code, $headers, $option),
            function ($response) use ($resource) {
                $response->original = $resource->resource->map(
                    function ($item) {
                        return is_array($item) ? Arr::get($item, 'resource') : $item->resource;
                    }
                );

                $resource->withResponse(request(), $response);
            }
        );
    }

    /**
     * Format JsonResource Data.
     *
     * @param  JsonResource  $resource
     * @param  string  $message
     * @param  int  $code
     * @param  array  $headers
     * @param  int  $option
     *
     * @return HigherOrderTapProxy|mixed
     */
    protected function formatResourceResponse($resource, string $message = '', $code = HttpResponse::HTTP_OK, array $headers = [], $option = 0)
    {
        return tap(
            response()->json($this->formatData($this->parseDataFrom($resource), $message, $code), $code, $headers, $option),
            function ($response) use ($resource) {
                $response->original = $resource->resource;

                $resource->withResponse(request(), $response);
            }
        );
    }

    /**
     * Parse data from JsonResource.
     *
     * @param  JsonResource  $data
     *
     * @return array
     */
    protected function parseDataFrom(JsonResource $data)
    {
        return array_merge_recursive($data->resolve(request()), $data->with(request()), $data->additional);
    }
}

在父类控制器中,引入 App/Http/Controllers

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use App\Support\Traits\ResponseTrait;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
    use ResponseTrait;
}

子控制器中使用

// 操作成功情况
$this->response->success($data,$message);
$this->response->success(new  UserCollection($resource),  '成功');// 返回 API Resouce Collection
$this->response->success(new  UserResource($user),  '成功');// 返回 API Resouce
$user  =  ["name"=>"nickname","email"=>"longjian.huang@foxmail.com"];
$this->response->success($user,  '成功');// 返回普通数组

$this->response->created($data,$message);
$this->response->accepted($message);
$this->response->noContent($message);

// 操作失败或异常情况
$this->response->fail($message);
$this->response->errorNotFound();
$this->response->errorBadRequest();
$this->response->errorForbidden();
$this->response->errorInternal();
$this->response->errorUnauthorized();
$this->response->errorMethodNotAllowed();

发表评论

电子邮件地址不会被公开。 必填项已用*标注