ark4ne/laravel-json-api 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

ark4ne/laravel-json-api

Composer 安装命令:

composer require ark4ne/laravel-json-api

包简介

A Lightweight JSON:API Resource for Laravel

README 文档

README

A Lightweight {JSON:API} Resource for Laravel.

example branch parameter codecov

Installation

composer require ark4ne/laravel-json-api

Config

Path Type Description
describer.nullable bool For describer notation, defined if a value is nullable by default.
describer.date string datetime format For describer notation, defined default date time format.
describer.precision int \ null For describer notation, decimal precision for float value. null for disable rounding.
describer.when-has bool \ string[] For describer notation, Apply automatically whenHas condition on attributes.
relationship.when-included bool Allow to disabled by default the loading of relationship data.

Usage

This package is a specialisation of Laravel's JsonResource class. All the underlying API's are still there, thus in your controller you can still interact with JsonApiResource classes as you would with the base JsonResource class

Request

This package allows the reading and dynamic inclusion of resources that will be requested in the requests via the "include" parameter.
@see {json:api} fetching-includes

Resource attributes will also be filtered according to the "fields" parameter.
@see {json:api} fetching-fields

You can also very simply validate your requests for a given resource via the rules Rules\Includes and Rules\Fields.

Include validation

use \Ark4ne\JsonApi\Requests\Rules\Includes;
use \Illuminate\Foundation\Http\FormRequest;

class UserFetchRequest extends FormRequest
{
    public function rules()
    {
        return [
            'include' => [new Includes(UserResource::class)],
        ]
    }
}

Rules\Includes will validate the include to exactly match the UserResource schema (determined by the relationships).

Fields validation

use \Ark4ne\JsonApi\Requests\Rules\Fields;
use \Illuminate\Foundation\Http\FormRequest;

class UserFetchRequest extends FormRequest
{
    public function rules()
    {
        return [
            'fields' => [new Fields(UserResource::class)],
        ]
    }
}

Rules\Fields will validate the fields to exactly match the UserResource schema (determined by the attributes and relationships).

Customize validation message

Trans key default
validation.custom.jsonapi.fields.invalid The selected :attribute is invalid.
validation.custom.jsonapi.fields.invalid_fields ":resource" doesn ' t have fields ":fields".
validation.custom.jsonapi.fields.invalid_resource ":resource" doesn ' t exists.
validation.custom.jsonapi.includes.invalid The selected :attribute is invalid.
validation.custom.jsonapi.includes.invalid_includes ":include" doesn ' t have relationship ":relation".

Resource

@see {json:api} resource-type

Implementable methods :

protected function toType(Request $request): string;

protected function toIdentifier(Request $request): int|string;

public function toAttributes(Request $request): iterable;

protected function toRelationships(Request $request): iterable;

protected function toResourceMeta(Request $request): ?iterable;

protected function toMeta(Request $request): ?iterable;

Example:

use Ark4ne\JsonApi\Resources\JsonApiResource;
use Illuminate\Http\Request;

class UserResource extends JsonApiResource
{
    public function toAttributes(Request $request): iterable
    {
        return [
            'name' => $this->name,
            'email' => $this->email,
        ];
    }

    protected function toResourceMeta(Request $request): ?iterable
    {
        return [
            'created_at' => $this->created_at->format(DateTimeInterface::ATOM),
            'updated_at' => $this->updated_at->format(DateTimeInterface::ATOM),
        ];
    }

    protected function toRelationships(Request $request): iterable
    {
        return [
            'posts' => PostResource::relationship(fn() => $this->posts, fn() => [
                'self' => "https://api.example.com/user/{$this->id}/relationships/posts",
                'related' => "https://api.example.com/user/{$this->id}/posts",
            ]),
            'comments' => CommentResource::relationship(fn() => $this->whenLoaded('comments')),
        ];
    }
}

toType

@see {json:api} resource-type

Returns resource type.

protected function toType(Request $request): string
{
    return 'user';
}

Default returns model class in kebab case : App\Models\MyPost => my-post

toIdentifier

@see {json:api} resource-identifier

Returns resource identifier.

protected function toIdentifier(Request $request): int|string
{
    return $this->id;
}

Default returns model id.

toAttributes

@see {json:api} resource-attributes

Returns resource attributes.

public function toAttributes(Request $request): iterable
{
    return [
        'name' => $this->name,
        'email' => $this->email,
    ];
}

Laravel conditional attributes

@see laravel: eloquent-conditional-attributes

Support laravel conditional attributes.

public function toAttributes(Request $request): array
{
    return [
        'name' => $this->name,
        'email' => $this->email,
        // with lazy evaluation
        'hash64' => fn() => base64_encode("{$this->id}-{$this->email}"),
        // Conditional attribute
        'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),       
        // Merging Conditional Attributes
        // use applyWhen insteadof mergeWhen for keep fields
        // useful for fields request rules validation
        $this->applyWhen($request->user()->isAdmin(), [
            'first-secret' => 123,
            'second-secret' => 456.789,
        ]),
    ];
}

Described attributes

@see described notation

public function toAttributes(Request $request): array
{
    return [
        'name' => $this->string(),
        // pass key to describer
        $this->string('email'),
        // with lazy evaluation
        'hash64' => $this->string(fn() => base64_encode("{$this->id}-{$this->email}")),
        // Conditional attribute
        $this->string('secret')->when($request->user()->isAdmin(), 'secret-value'),       
        // Merging Conditional Attributes
        $this->applyWhen($request->user()->isAdmin(), [
            'first-secret' => $this->integer(fn() => 123),
            'second-secret' => $this->float(fn() => 456.789),
        ]),
    ];
}

toRelationships

@see {json:api} resources-relationships

Returns resource relationships.

All relationships must be created with ModelResource::relationship. This allows the generation of the schema representing the resource and thus the validation of request includes.

If your relation should have been a collection created via the ::collection(...) method, you can simply use ->asCollection().

If you want the relation data to be loaded only when it is present in the request include, you can use the ->whenIncluded() method.

protected function toRelationships(Request $request): array
{
    return [
        'avatar' => AvatarResource::relationship($this->avatar),
        // with conditional relationship
        'administrator' => $this->when($request->user()->isAdmin(), UserResource::relationship(fn() => $this->administrator),
        // as collection, with conditional value
        'comments' => CommentResource::relationship(fn() => $this->whenLoaded('comments'))->asCollection(),
        // with relationship (allow to include links and meta on relation)
        'posts' => PostResource::relationship(fn() => $this->posts)->withLinks(fn() => [
            'self' => "https://api.example.com/user/{$this->id}/relationships/posts",
            'related' => "https://api.example.com/user/{$this->id}/posts",
        ])->asCollection(),
    ];
}

toRelationships must returns an array, keyed by string, of JsonApiResource or JsonApiCollection.

Laravel conditional relationships

@see laravel: eloquent-conditional-relationships

Support laravel conditional relationships.

protected function toRelationships(Request $request): array
{
    return [
        'avatar' => AvatarResource::relationship($this->avatar),
        // as collection, with condition
        'comments' => CommentResource::relationship(fn() => $this->whenLoaded('comments'))->asCollection(),
        // with relationship (allow to include links and meta on relation)
        'posts' => PostResource::relationship(fn() => $this->posts)
                ->asCollection(),
    ];
}

Described attributes

@see described notation

protected function toRelationships(Request $request): array
{
    return [
        'avatar' => $this->one(AvatarResource::class),
        // custom relation name
        'my-avatar' => $this->one(AvatarResource::class, 'avatar'),
        // as collection, with condition
        'comments' => $this->many(CommentResource::class)
                           ->whenLoaded(),
        // with relationship (allow to include links and meta on relation)
        'posts' => $this->many(PostResource::class)
                ->links(fn() => [
                    'self' => "https://api.example.com/posts/{$this->resource->id}/relationships/posts",
                    'related' => "https://api.example.com/posts/{$this->resource->id}/posts",
                ])
                ->meta(fn() => [
                    'total' => $this->integer(fn() => $this->resource->posts()->count()),
                ]),
    ];
}

Relation links and meta

@see {json:api}: relation-linkage
@see {json:api}: relation-meta

Returns links and meta for a relation.

protected function toRelationships(Request $request): array
{
    return [
        'posts' => PostResource::relationship(fn() => $this->posts)->withLinks(fn() => [
            // links
            'self' => "https://api.example.com/user/{$this->id}/relationships/posts",
            'related' => "https://api.example.com/user/{$this->id}/posts",
        ])->withMeta(fn() => [
            // meta
            'creator' => $this->name,
        ])
        ->asCollection(),
    ];
}

toLinks

@see {json:api}: resource-linkage

Returns resource links.

protected function toLinks(Request $request): ?array
{
    return [
        'self' => route('api.user.show', ['id' => $this->id]),
    ];
}

toResourceMeta

@see {json:api}: resource-meta
@see {json:api}: document-meta

Returns resource meta.

protected function toResourceMeta(Request $request): ?iterable
{
    return [
        'created_at' => $this->created_at->format(DateTimeInterface::ATOM),
        'updated_at' => $this->updated_at->format(DateTimeInterface::ATOM),
    ];
}

toMeta

@see {json:api}: document-meta

Returns document meta.

protected function toMeta(Request $request): ?iterable
{
    return [
        "copyright": "Copyright 2022 My Awesome Api",
    ];
}

Collection

@see laravel: resource-collection

Collection are implemented in JsonApiCollection.

Usage is the same as laravel collections.

UserResource::collection(User::all()); // => JsonApiCollection

Described notation

Value methods

Method Description
bool Cast to boolean
integer Cast to integer
float Cast to float
string Cast to string
date Cast to date, allow to use custom format
array Cast to array, supports typed arrays with ->of()
arrayOf Helper method for typed arrays (alternative to array()->of())
mixed Don't cast, return as is
enum Get enum value
struct Custom struct. Accept an array of values

Relation methods

Method Description
one For relationship with a single value: HasOne, BelongsTo, ...
many For relationship with many value: HasMany, BelongsToMany, ...

Enum

Method enum allow to get enum value for backed enum or name for unit enum.

According to structure:

/// Role.php
enum Role {
    case ADMIN;
    case USER;
}
/// State.php
enum State:int {
    case ACTIVE = 1;
    case INACTIVE = 0;
}
/// User.php
class User extends Model
{
    $casts = [
        'role' => Role::class,
        'state' => State::class,
    ];
}

The following attributes resource:

// UserResource.php
public function toAttributes(Request $request): array
{
    return [
        'status' => $this->enum(),
        'role' => $this->enum(),
    ];
}

Will return:

[
    "status": 1,
    "role": "ADMIN"
]

Typed Arrays

The array descriptor supports typed arrays to ensure all elements are cast to a specific type. This is useful when you need to guarantee type consistency across array elements.

Basic Usage

// UserResource.php
public function toAttributes(Request $request): array
{
    return [
        // Array of strings - all values will be cast to string
        'tags' => $this->array('tags')->of($this->string()),

        // Array of integers - all values will be cast to integer
        'scores' => $this->array('scores')->of($this->integer()),

        // Array of floats
        'prices' => $this->array('prices')->of($this->float()),

        // Array of booleans
        'flags' => $this->array('flags')->of($this->bool()),
    ];
}

Using Class References

You can also use class references instead of descriptor instances:

use Ark4ne\JsonApi\Descriptors\Values\ValueString;
use Ark4ne\JsonApi\Descriptors\Values\ValueInteger;

public function toAttributes(Request $request): array
{
    return [
        'tags' => $this->array('tags')->of(ValueString::class),
        'scores' => $this->array('scores')->of(ValueInteger::class),
    ];
}

Alternative Syntax

You can also use the arrayOf() helper method:

public function toAttributes(Request $request): array
{
    return [
        'tags' => $this->arrayOf($this->string(), 'tags'),
        'scores' => $this->arrayOf($this->integer(), 'scores'),
    ];
}

Nested Typed Arrays

For multi-dimensional arrays, you can nest array()->of() calls:

public function toAttributes(Request $request): array
{
    return [
        // 2D array (matrix) of integers
        'matrix' => $this->array('matrix')->of(
            $this->array()->of($this->integer())
        ),
    ];
}

With Closures and Transformations

Combine typed arrays with closures for data transformation:

public function toAttributes(Request $request): array
{
    return [
        // Transform and type cast
        'doubled' => $this->array(fn() => array_map(fn($n) => $n * 2, $this->numbers))
            ->of($this->integer()),

        // Access nested properties
        'user_ids' => $this->array(fn() => $this->users->pluck('id'))
            ->of($this->integer()),
    ];
}

With Conditions

Typed arrays support all conditional methods:

public function toAttributes(Request $request): array
{
    return [
        // Only include if not null
        'tags' => $this->array('tags')->of($this->string())->whenNotNull(),

        // Only include if array is not empty
        'scores' => $this->array('scores')->of($this->integer())->whenFilled(),

        // Conditional based on closure
        'admin_notes' => $this->array('notes')->of($this->string())
            ->when(fn() => $request->user()->isAdmin()),
    ];
}

⚠️ Important Note: Conditions applied to the item type (inside of()) are not evaluated per-item. They apply to the entire array descriptor, not individual elements.

// ❌ This will NOT filter individual items
'even-numbers' => $this->array('numbers')->of(
    $this->integer()->when(fn($request, $model, $attr) => $attr % 2 === 0)
)
// All items will be included, the when() doesn't filter per item

// ✅ To filter items, do it before passing to the array
'even-numbers' => $this->array(
    fn() => array_filter($this->numbers, fn($n) => $n % 2 === 0)
)->of($this->integer())

Example

Given a model with mixed-type arrays:

$user = new User([
    'tags' => ['php', 'laravel', 123, true],
    'scores' => [95.5, '87', 92, '78.9'],
]);

The resource will ensure type consistency:

public function toAttributes(Request $request): array
{
    return [
        'tags' => $this->array('tags')->of($this->string()),
        'scores' => $this->array('scores')->of($this->integer()),
    ];
}

Output:

{
    "tags": ["php", "laravel", "123", "1"],
    "scores": [95, 87, 92, 78]
}

ark4ne/laravel-json-api 适用场景与选型建议

ark4ne/laravel-json-api 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 21.62k 次下载、GitHub Stars 达 7, 最近一次更新时间为 2022 年 04 月 05 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「JSON-API」 「jsonapi.org」 「laravel-json」 「laravel-resource」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 ark4ne/laravel-json-api 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 ark4ne/laravel-json-api 我们能提供哪些服务?
定制开发 / 二次开发

基于 ark4ne/laravel-json-api 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 21.62k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 7
  • 点击次数: 13
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 7
  • Watchers: 1
  • Forks: 2
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2022-04-05