serhiikamolov/laravel-jsonapi

一套接口和类,用于辅助使用Laravel框架构建高效的JSON:API应用程序。

v4.1 2024-03-26 18:01 UTC

README

一套接口和类,用于辅助使用Laravel框架构建高效的JSON:API应用程序。

Latest Stable Version License

目录

安装

通过Composer安装此包非常简单,只需在项目根目录下运行即可

composer require serhiikamolov/laravel-jsonapi

自定义错误处理器

为了使错误输出与json API格式兼容,请转到bootstrap/app.php并注册自定义错误处理器。

$app->singleton(
   Illuminate\Contracts\Debug\ExceptionHandler::class,
   \JsonAPI\Exceptions\Handler::class
);

或者直接从JsonAPI\Exceptions\Handler类扩展默认的App\Exceptions\Handler

现在,在发生异常的情况下,您将得到以下响应

{
    "links": {
        "self": "http://127.0.0.1/api/v1/auth/login"
    },
    "errors": {
        "messages": [
            "Some internal exception"
        ]
    },
    "debug": {
        "message": "Some internal exception",
        "exception": "Exception",
        "file": "/code/app/Http/Controllers/AuthController.php",
        "line": 29,
        "trace": [...]
    }
}

请求验证类

\JsonAPI\Contracts\RequestFormRequest类的一个简单扩展,它返回与json API格式兼容的验证错误

namespace App\Http\Requests\Auth;

use \JsonAPI\Contracts\Request;

class LoginRequest extends Request
{

    public function messages()
    {
        return [
            'email.required'  => 'Значення e-mail не може бути порожнім',
            'email.email'   => 'Значення e-mail не відповідає формату електронної пошти',
            'email.max'     => 'Значення e-mail не має бути таким довгим',
        ];
    }
    
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules():array
    {
        return [
            'email' => 'required|email|max:255',
            'password' => 'required|string',
        ];
    }

}

具有请求验证的控制器示例

namespace App\Http\Controllers;

use JsonAPI\Response\Response;
use App\Http\Requests\Auth\LoginRequest;

class AuthController extends Controller
{
    /**
     * Request a JWT token
     *
     * @param LoginRequest $request
     * @param Response $response
     * @return JsonResponse
     */
    public function login(LoginRequest $request, Response  $response):Response
    {
        // validation is passed, you can check the user credentials now
        // and generate a JWT token 
    }      
}

包含验证错误的响应

{
    "links": {
        "self": "http://127.0.0.1/api/v1/auth/login"
    },
    "errors": {
        "email": [
            "The email field is required."
        ]
    }
}

响应类

JsonAPI\Response\ResponseJsonResponse类的一个扩展,具有一些附加方法。

向响应的links对象添加额外值

$response->links($array)

在响应中返回带有特定代码的错误

$response->error($statusCode, $message)  

返回带有JWT令牌的响应

$response->token($token, $type = 'bearer', $expires = null)

在响应中返回自定义数据对象

$response->data($array)

将字段附加到响应的数据对象

$response->attach($key, $value)

向响应对象添加调试信息

$response->debug($array)

向响应对象添加元数据

$response->meta($array, $key = 'meta')

序列化Eloquent集合或数据模型

$response->serialize($collection, $serializer = new Serializer())

在响应中分页数据数组

$response->serialize($collection)->paginate()

向响应添加特定状态码

$response->code($statusCode)

Response类实现了Builder模式,因此您可以连续使用不同的方法。

public function login(LoginRequest $request, Response $response): Response
{
    ...
    return $response
        ->token((string)$token)
        ->attach('uuid', Auth::guard('api')->user()->uuid);
}

响应结果

{
    "links": {
        "self": "http://127.0.0.1/api/v1/auth/login"
    },
    "data": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC8xMjcuMC4wLjFcL2FwaVwvdjFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNjA4Mzc0NTAyLCJleHAiOjE2MDgzNzQ1NjIsIm5iZiI6MTYwODM3NDUwMiwianRpIjoiUXRZWnpUeEhYajJyaGxHaCIsInN1YiI6MiwicHJ2IjoiY2U2ZTY0NDI4OTkwYjk4NWJjZTQ2OGZjNTlmZTUxYzNiZTljN2ZhZCJ9.PNF2-5nswNqaWS4WS1D_gemBD3IyPJVKXJohNF8mMUY",
        "token_type": "bearer",
        "expires_in": 60,
        "uuid": "40e831349e594d8e944478a243ff463f"
    }
}

序列化类

序列化器允许将复杂的数据,如Eloquent集合和模型实例,转换为简单的数组,这些数组可以轻松地渲染为JSON。序列化器类为您提供了一种强大的、通用的方式来控制响应的输出。

声明序列化器

让我们创建一个简单的序列化器类,它扩展了JsonAPI\Response\Serializer类。有一个公开的fields()方法,它返回一个数组,并定义一组字段,这些字段将从给定的集合或模型中检索出来,并将其放入响应中。

namespace App\Http\Serializers;

use JsonAPI\Response\Serializer;

class UserSerializer extends Serializer
{
    public function fields(): array
    {
        return [
            'id',    // take data from $user->id
            'name',  // take data from $user->name
            'email', // take data from $user->email
            'uuid'   // take data from the public method defined below
        ];
    }

    /**
    * Define a custom field 
    */
    public function uuid(Model $item): string
    {
        return md5($item->id);
    }
}

您会注意到,这里的uuid不是从数据库中获取的,而是从模型数据中生成的。这样,您可以定义响应中需要的新字段,甚至可以覆盖现有字段的值。

使用模型序列化器

JsonAPI\Response\Response类中有一个serialize方法,它接受一个序列化器实例作为第二个参数。

class UsersController extends Controller
{
    /**
     * Get list of all users.
     *
     * @param Response $response
     * @return Response
     */
    public function read(Response $response): Response
    {
        $users = User::all();
        return $response->serialize($users, new UserSerializer());
    }
}

响应结果

{
    "links": {
        "self": "http://127.0.0.1/api/v1/users"
    },
    "data": [
        {
            "id": 1,
            "name": "admin",
            "email": "user@email.com",
            "uuid": "40e831349e594d8e944478a243ff463f"
        }
    ]
}

使用字段修饰符

字段修饰符可以应用于序列化器中定义的每个字段。尽管如此,还有一些预定义的修饰符:timestampnumbertrim,您可以通过创建一个具有modifier前缀的受保护方法来定义自己的修饰符。此外,您还可以使用另一个序列化类作为修饰符,这当您有一些与原始模型相关的相关数据时非常有用。

class UserSerializer extends Serializer
{
    public function fields(): array
    {
        return [
            'id' => 'md5'                       // use custom modifier
            ...
            'created_at' => 'timestamp',        // use default modifier which 
                                                // transforms a Carbon date object 
                                                // into the unix timestamp number
            'roles' => RoleSerializer::class    // use a serializing class as a modifier
                                                // for the related data
        ];
    }

     /**
     * Define custom modifier which transforms user id to md5 hash.
     * @param int|null $value
     * @return int
     */
    protected function modifierMd5(?int $value): string
    {
        return md5($value);
    }
}

使用查询日志扩展响应

启用 JsonAPI\Http\Middleware\JsonApiDebug 中间件,并将查询日志的信息扩展到响应的 debug 部分。

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middlewareGroups = [
        ...
        'api' => [
            ...
            \JsonAPI\Http\Middleware\JsonApiDebug::class                  
        ],
        ...
    ];
}

包含查询日志的响应。

{
  ...
  "debug": {
    "queries": {
        "total": 2,
        "list": [
            {
                "query": "select * from `users` where `id` = ? limit 1",
                "bindings": [
                    2
                ],
                "time": 20.81
            },
            {
                "query": "select `roles`.*, `role_user`.`user_id` as `pivot_user_id`, `role_user`.`role_id` as `pivot_role_id`, `role_user`.`created_at` as `pivot_created_at`, `role_user`.`updated_at` as `pivot_updated_at` from `roles` inner join `role_user` on `roles`.`id` = `role_user`.`role_id` where `role_user`.`user_id` = ? and `roles`.`deleted_at` is null",
                "bindings": [
                    2
                ],
                "time": 73.97
            }
        ]
    }
  }
}

测试API响应

JsonAPI\Traits\Tests\JsonApiAsserts 特性添加到您的默认 TestCase 类中,并使用一些有用的断言方法扩展您的测试以测试API响应。

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use JsonAPI\Traits\Tests\JsonApiAsserts;

abstract class TestCase extends BaseTestCase
{
    use JsonApiAsserts;
}

使用附加断言的示例

    /**
     * Testing GET /api/v1/entries/<id>
     */
    public function test_read()
    {
        $response = $this->get("/api/v1/entries/1");

        // expecting to get response in JSON:API format and 
        // find "id", "value", "type", "active" fields within 
        // a response's data
        $this->assertJsonApiResponse($response, [
            "id",
            "value",
            "type",
            "active",
        ]);
    }