Lightweight JSON:API resource for Laravel

Related tags

Laravel json-api
Overview

JSON:API Resource for Laravel

A lightweight Laravel implementation of JSON:API.

This is a WIP project currently being built out via livestream on my YouTube channel. Come hang out next stream.

TODO

  • Top-level collections
  • Nested collections
  • Caching for performance (use a weakmap with the request as the key?)
  • Document that this is to be used in conjunction with Spatie Query Builder
  • document why whenLoaded isn't great
  • How to handle single resources loading / allow listing (can we PR Spatie Query Builder for this or does it already support it?).

Basic usage

Identification

JSON:API docs: Identification

The "id" and "type" of a resource is automatically resolved for you under-the-hood if you are using resources solely with Eloquent models.

The default bindings resolve the "id" by calling (string) $model->getKey() and they resolves the "type" by using a camel case of the model's table name, e.g. blog_posts becomes blogPosts.

Nice. Well that was easy, so let's move onto...

Attributes

JSON:API docs: Attributes

To provide a set of attributes for a resource, you can implement the toAttributes(Request $request) method...



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

Relationships

JSON:API docs: Relationships

Just like we saw with attributes above, we can specify relationships that should be available on the resource by using the toRelationships(Request $request) method, however with relationships you should always wrap the values in a Closure.



class UserResource extends JsonApiResource
{
    /**
     * @return array
     */
    public function toRelationships(Request $request): array
    {
        return [
            'posts' => fn () => PostResource::collect($this->posts),
            'subscription' => fn () => SubscriptionResource::make($this->subscription),
        ];
    }
}

Including relationships

JSON:API docs: Inclusion of Related Resources

These relationships however, are not included in the response unless the calling client requests them. To do this, the calling client needs to "include" them by utilising the include query parameter.

# Include the posts...
/api/users/8?include=posts

# Include the comments...
/api/users/8?include=comments

# Include both...
/api/users/8?include=posts,comments

Note: In the advanced usage you can learn how to include relationships in the response without them being included by the client.

Advanced usage

Identification

Customising the "id" resolver

You can change the "id" resolver via a service provider by binding your own implementation of the ResourceIdResolver, which can be fulfilled by any callable. The callable receives the Resource Object as it's first parameter.



class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(ResourceIdResolver::class, fn () => function (mixed $resourceObject): string {
            if ($resourceObject instanceof Model) {
                return (string) $resourceObject->getKey();
            }

            if ($resourceObject instanceof ValueObject) {
                return (string) $resourceObject->getValue();
            }

            if (is_object($resourceObject)) {
                throw new RuntimeException('Unable to resolve Resource Object id for class '.$resourceObject::class);
            }

            throw new RuntimeException('Unable to resolve Resource Object id for type '.gettype($resourceObject));
        });
    }
}

Customising the "type" resolver

You can change the "type" resolver via a service provider by binding your own implementation of the ResourceTypeResolver, which can be fulfilled by any callable. The callable receives the Resource Object as it's first parameter.



class AppServiceProvider
{
    public function register()
    {
        $this->app->singleton(ResourceTypeResolver::class, fn () => function (mixed $resourceObject): string {
            if (! is_object($resourceObject)) {
                throw new RuntimeException('Unable to resolve Resource Object type for type '.gettype($resourceObject));
            }

            return match($resourceObject::class) {
                User::class => 'users',
                Post::class => 'posts',
                Comment::class => 'comments',
                default => throw new RuntimeException('Unable to resolve Resource Object type for class '.$resourceObject::class),
            };
        });
    }
}

Attributes

Sparse fieldsets

JSON:API docs: Sparse fieldsets

Without any work, your response supports sparse fieldsets. If you are utilising sparse fieldsets and have some attributes that are expensive to create, it is a good idea to wrap them in a Closure. Under the hood, we only call the Closure if the attribute is to be included in the response.



class UserResource extends JsonResource
{
    public function toAttributes(Request $request): array
    {
        return [
            'name' => $this->name,
            'email' => $this->email,
            'profile_image' => fn () => base64_encode(
                // don't really download a file like this. It's just an example of a slow operation...
                file_get_contents('https://www.gravatar.com/avatar/'.md5($this->email)),
            ),
        ];
    }
}

The Closure is only called when the attribute is going to be included in the response, which improves performance of requests that don't require the returned value.

# The Closure is not called...
/api/users/8?fields[users]=name,email

# The Closure is called...
/api/users/8?fields[users]=name,profile_image

Relationships

JSON:API docs: Inclusion of Related Resources

Relationships can be resolved deeply and also multiple relationship paths can be included. Of course you should be careful about n+1 issues, which is why we recommend using this package in conjunction with Spatie's Query Builder.

# Including deeply nested relationships
/api/posts/8?include=author.comments

# Including multiple relationship paths
/api/posts/8?include=comments,author.comments
Comments
  • Doesn't show the attributes of a relationship

    Doesn't show the attributes of a relationship

    Hi Tim.

    Do you have any idea why don't the attributes of a relationship? I'm testing two API's and both with the same result. I follow the instructions but doesn't work. 12-4-2022 20 4 35 1

    I would appreciate any help

    opened by jonquintero 7
  • Package requires Laravel framework instead of Illuminate packages

    Package requires Laravel framework instead of Illuminate packages

    As highlighted in composer.json https://github.com/timacdonald/json-api/blob/c3f89eef3e6c3e6b4f46321211543acc99f5134f/composer.json#L20 this package requires laravel/framework.

    Trying to use this package in a Luman application is resulting in Illuminate packages being removed and laravel/framework being installed as its replacement causing some application issues.

    I think it would be relatively easy to replace the dependencies for the sub packages of Laravel framework. Before creating such an PR I wanted to check if you would be accepting pull requests for this?

    opened by DannyvdSluijs 3
  • Update infection/infection requirement from ^0.24.0 to ^0.25.3

    Update infection/infection requirement from ^0.24.0 to ^0.25.3

    Updates the requirements on infection/infection to permit the latest version.

    Release notes

    Sourced from infection/infection's releases.

    Ignore mutations by regex for uncovered mutants

    Fixed:

    • ignoreSourceCodeByRegex option is ignored between // @codeCoverageIgnoreStart and // @codeCoverageIgnoreEnd #1561
    Commits
    • f7a1af2 Ignore mutations by regex for uncovered mutants (#1585)
    • c3e23df Fix boolean issues reported by PHPStan (#1584)
    • 15015ab Do not use ocramius/package-versions (and do not break building PHAR this tim...
    • 20ba7d0 Run e2e tests on GH Actions with prefixed PHAR in addition to bin/infection (...
    • 69c1761 Revert "Do not use ocramius/package-versions " (#1581)
    • 353ebc6 CS
    • fcf3df8 Make it clear we're not touching max int negative
    • 25ee14d Do not touch -PHP_INT_MAX
    • 4ffe8d5 Update comments
    • b6509d2 cs
    • Additional commits viewable in compare view

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 2
  • Add missing slash to JSON:API spec URL

    Add missing slash to JSON:API spec URL

    Currently the URL directs to github.com/jsonapi.org which (in addition to not being what is desired) results in a blank page being displayed after the GitHub back-end rejects the route.

    opened by lukepfjo 1
  • Update infection/infection requirement from ^0.24.0 to ^0.25.4

    Update infection/infection requirement from ^0.24.0 to ^0.25.4

    Updates the requirements on infection/infection to permit the latest version.

    Release notes

    Sourced from infection/infection's releases.

    bypass-finals conflic, automatic XDEBUG_MODE=coverage, stop Infection execution on empty git diff filter

    Added:

    Changed:

    Changelog

    Sourced from infection/infection's changelog.

    0.25.4 (2021-12-08)

    Added:

    Changed:

    0.25.0 (2021-09-05)

    Full Changelog

    Added:

    • Detect syntax errors during mutation analysis and differentiate them from all errors #1555 #262
    • Add $schema to generated infection.json config file for autocomplete #1553 #1432

    Changed:

    • [Performance] Add files to coverage whitelist instead of the whole directories when --filter or --git-diff-filter are used #1543
    • [Performance] Speed up Infection runs by remembering which test killed a mutant #1519 #1549
    • [internal] Allow Infection test suite to be executed in parallel using Paratest #1544
    • Generate infection.json (without .dist postfix) by default #1554
    • Do not mark Mutant as Killed when no tests were executed #1546

    Fixed:

    • Display time and consumed memory even in case of insufficient MSI #1562
    • Trim "\n" and "\t" characters when replacing relative paths with absolute ones during XML config creation #1550 #1542
    • For Mutant's phpunit.xml, set executionOrder="default" to prevent random ordering of the tests since we need them to be sorted (fastest - first) #1547

    0.24.0 (2021-07-25)

    Full Changelog

    Added:

    • [Mutator] Add Mutator SpreadAssignment #1529
    • [Mutator] Add Mutator SpreadRemoval #1529

    Changed:

    • [Performance] Improve Infection performance executed against slow test suites #1539
    • Allow using MSI Badge for multiple branches #1538
    • Add Mutator information to GitHub annotation logger #1540
    • [BC BREAK] Rename Spread mutator to SpreadOneItem #1529

    ... (truncated)

    Commits
    • 88dae3c Update CHANGELOG.md for 0.25.4
    • a9e7c20 Allow Symfony 6 (#1606)
    • f60fbc5 Add dg/bypass-finals to the conflict packages list (#1605)
    • 735cd3b Update Psalm and bump errorLevel to 6 (#1532)
    • 847f05d Relax version requirements for the pipeline library (#1603)
    • 2da5bcc Set XDEBUG_MODE for processes with coverage (#1518)
    • 1892158 feat: Concat does not generate mutant when both operands are the same (#1602)
    • a7ece6f Stop Infection execution with 0 exit code when git diff filter returns empt...
    • 84140e6 PHPStan fixes (#1595)
    • 384d439 Run integration tests after E2E ones (#1596)
    • Additional commits viewable in compare view

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 0
  • To Many relationship wrong wrapped?

    To Many relationship wrong wrapped?

    Hi,

    Im using a relationship that is an collection. If i look into the response relationship attributes i get "relationships": { "order_lines": [ { "data": { "id": "12", "type": "orderLines", "meta": [] }, "meta": [], "links": [] } ]

    When looking into the json spec it should be like

    "comments": { "data": [ { "type": "comments", "id": "5" }, { "type": "comments", "id": "12" } ] }

    Why is it wrapping it with an array instead of wrapping the data with the array? Am i missing something or doing it wrong? Or just a bug

    opened by bolivir 6
  • Class

    Class "TiMacDonald\JsonApi\JsonApiResource" not found

    Hi there,

    So i've picked up this package this morning and installed into my project, made the necessary change to my resource file and im simply getting the error: Class "TiMacDonald\JsonApi\JsonApiResource" not found

    This is my resource file

    <?php
    
    namespace App\Http\Resources;
    
    use Illuminate\Http\Request;
    use TiMacDonald\JsonApi\JsonApiResource;
    
    class PostResource extends JsonApiResource
    { 
    ...
    

    Am I missing something here? I can see the files in the vendor folder etc but just doesn't work. I've tried composer dump-autoload and that hasn't resolved anything either.

    Regards

    opened by CNSam 1
  • Top level Meta for Collection

    Top level Meta for Collection

    Even though this is specifically in the To Do section, I'm making an issue for it to bump the urgency of it as I need it to pass back some additional data.

    I am imagining an API that looks something like this: Resource::collection($data)->withMeta($meta)

    Again, if @timacdonald has a specific implementation in mind, let me know. If it sounds good in theory, I can try to take a stab at it.

    opened by snellingio 1
  • `include` without query string

    `include` without query string

    Currently, there is not a way to include relationships without explicitly asking for them. While this makes sense in almost all cases, I have a unique case where on a specific resource I want to include some morphTo's in the include by default.

    Where includes happen: https://github.com/timacdonald/json-api/blob/c3f89eef3e6c3e6b4f46321211543acc99f5134f/src/Support/Includes.php#L38

    Options could be adding a default to the $request->query('include'), which would work for my use case, but probably wouldn't be flexible enough for most people where they would want to ALWAYS have a default include even when passing in more.

    I'll think a little bit about it, but I imagine that @timacdonald will have a better / specific implementation that he wants. If you can describe the best way to do it, I could take a shot at it.

    opened by snellingio 2
  • Fix for included collection with numeric keys

    Fix for included collection with numeric keys

    When using the Resource Collection, if you have a relationship that is a HasOne or BelongsTo, and the keys are numeric, the flattened include map has keys associated with it.

    This only happens with numeric keys, and does not happen when the keys are strings (as in all test cases)

    Example: User -> HasOne? -> License

    Without values() called, you'll end up with a keyed included response:

      "included": {
        "0": {
          "id": "1",
          "type": "basicModels",
          "attributes": {
            "url": "https://example.com/avatar1.png"
          },
          "relationships": {},
          "meta": {},
          "links": {}
        },
        "2": {
          "id": "2",
          "type": "basicModels",
          "attributes": {
            "url": "https://example.com/avatar2.png"
          },
          "relationships": {},
          "meta": {},
          "links": {}
        }
      },
    

    This can be solved by getting only the values() from the collection.

    You'll see that this does not occur when making a single resource, because values is already called on the included method:

    https://github.com/timacdonald/json-api/blob/main/src/JsonApiResource.php#L170

    https://github.com/timacdonald/json-api/blob/c3f89eef3e6c3e6b4f46321211543acc99f5134f/src/Concerns/Relationships.php#L55

    opened by snellingio 5
Releases(v0.2.1)
Owner
Tim MacDonald
Developing engaging and performant web applications with a focus on TDD. Specialising in PHP / Laravel projects. ❤️ building for the web.
Tim MacDonald
CORS (Cross-Origin Resource Sharing) support for Laravel and Lumen

Description This package adds Cross-Origin Resource Sharing (CORS) support to your Laravel application. The package is based on Framework agnostic (PS

null 48 Feb 1, 2020
⚙️Laravel Nova Resource for a simple key/value typed setting

Laravel Nova Resource for a simple key/value typed setting Administer your Laravel Simple Setting in Nova Pre-requisites This Nova resource package re

elipZis 5 Nov 7, 2022
This package provides a Filament resource to view all Laravel sent emails.

This package provides a Filament resource to view all Laravel outgoing emails. It also provides a Model for the database stored emails. Installation Y

Ramón E. Zayas 22 Jan 2, 2023
Trait for multilingual resource file support

⚡ Usage This library supports MultilingualResourceTrait which can be used in PluginBase. Multilingual support of resource files is possible using this

PocketMine-MP projects of PresentKim 1 Jun 7, 2022
Simply define the permission in the filament resource.

Simply define the permissions in the filament resource. Easily define permissions for Filament Resources & Relation Managers Installation You can inst

Ziyaan 8 Nov 16, 2022
A lightweight package for handling API error responses.

Laravel API Errors This package provides an easy way to manage and handle error response for JSON API's. Installation You can install the package via

3 SIDED CUBE 2 Feb 9, 2022
JSON-RPC 2.0 API server for @Laravel framework

Sajya is an open-source project aiming to implement the JSON-RPC 2.0 server specification for the Laravel quickly.

Sajya 179 Dec 29, 2022
JSON:API for Laravel applications

JSON:API for Web Artisans Implement feature-rich JSON:API compliant APIs in your Laravel applications. Build your next standards-compliant API today.

Laravel JSON:API 330 Dec 26, 2022
Renders consistent HTTP JSON responses for API-based projects

Laravel API Response is a package that helps to provide and render a consistent HTTP JSON responses to API calls as well as converting and formatting

Kennedy Osaze 43 Nov 20, 2022
Jetstrap is a lightweight laravel 8 package that focuses on the VIEW side of Jetstream / Breeze package installed in your Laravel application

A Laravel 8 package to easily switch TailwindCSS resources generated by Laravel Jetstream and Breeze to Bootstrap 4.

null 686 Dec 28, 2022
A lightweight domain event pattern implementation for Doctrine2.

Knp Rad Domain Event A lightweight domain event pattern implementation for Doctrine2. Official maintainers: @Einenlum Installation With composer : $ c

KNP Labs 5 Sep 23, 2022
A lightweight PHP paginator, for generating pagination controls in the style of Stack Overflow and Flickr.

PHP Paginator A lightweight PHP paginator, for generating pagination controls in the style of Stack Overflow and Flickr. The "first" and "last" page l

Jason Grimes 370 Dec 21, 2022
Builds nice, normalized and easy to consume REST JSON responses for Laravel powered APIs.

REST API Response Builder for Laravel Master branch: Development branch: Table of contents Introduction Why should I use it? Usage examples Features E

Marcin Orlowski 614 Dec 26, 2022
🔐 JSON Web Token Authentication for Laravel & Lumen

Documentation Documentation for 1.* here For version 0.5.* See the WIKI for documentation. Supported by Auth0 If you want to easily add secure authent

Sean Tymon 10.7k Jan 3, 2023
⚙️Simple key/value typed settings for your Laravel app with synchronized json export

Simple key/value typed settings for your Laravel app Create, store and use key/value settings, typed from numbers over dates to array, cached for quic

elipZis 8 Jan 7, 2023
Source code behind the Laracasts Larabit: Using MySQL JSON Columns with Laravel

Using MySQL JSON Columns with Laravel This is the source code behind the Laracasts Larabit: Using MySQL JSON Columns with Laravel, and features all of

Andrew Schmelyun 2 Dec 24, 2021
Store your Laravel application settings in an on-disk JSON file

Store your Laravel application settings in an on-disk JSON file. This package provides a simple SettingsRepository class that can be used to store you

Ryan Chandler 24 Nov 16, 2022
Get estimated read time of an article. Similar to medium.com's "x min read". Multilingual including right-to-left written languages. Supports JSON, Array and String output.

Read Time Calculates the read time of an article. Output string e.g: x min read or 5 minutes read. Features Multilingual translations support. Static

Waqar Ahmed 8 Dec 9, 2022
PHP Simple Response, XML, JSON,... auto response with accept in request's header

simple-response Simple package to handle response properly in your API. This package does not include any dependency. Install Via Composer $ composer

Phuong Danh 3 Dec 8, 2021