bchalier/laravel-json-api

A Lightweight JSON:API Resource for Laravel

1.5.0 2024-03-21 10:40 UTC

This package is auto-updated.

Last update: 2024-04-21 10:51:18 UTC


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;

protected 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
{
    protected 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.

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

Laravel conditional attributes

@see laravel: eloquent-conditional-attributes

Support laravel conditional attributes.

protected 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

protected 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
mixed Don't cast, return as is
enum Get enum value.

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
protected function toAttributes(Request $request): array
{
    return [
        'status' => $this->enum(),
        'role' => $this->enum(),
    ];
}

Will return:

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