Coder Social home page Coder Social logo

swisnl / json-api-client Goto Github PK

View Code? Open in Web Editor NEW
203.0 14.0 24.0 736 KB

A PHP package for mapping remote {json:api} resources to Eloquent like models and collections.

License: MIT License

PHP 100.00%
json-api hydration client json psr laravel-inspired hacktoberfest

json-api-client's Introduction

{ json:api } Client

PHP from Packagist Latest Version on Packagist Software License Buy us a tree Build Status Scrutinizer Coverage Scrutinizer Code Quality Made by SWIS

A PHP package for mapping remote JSON:API resources to Eloquent like models and collections.

💡 Before we start, please note that this library can only be used for JSON:API resources and requires some basic knowledge of the specification. If you are not familiar with {json:api}, please read the excellent blog by Björn Brala for a quick introduction.

Installation

ℹ️ Using Laravel? Take a look at swisnl/json-api-client-laravel for easy Laravel integration.

composer require swisnl/json-api-client

N.B. Make sure you have installed a PSR-18 HTTP Client and PSR-17 HTTP Factories before you install this package or install one at the same time e.g. composer require swisnl/json-api-client guzzlehttp/guzzle:^7.3.

HTTP Client

We are decoupled from any HTTP messaging client with the help of PSR-18 HTTP Client and PSR-17 HTTP Factories. This requires an extra package providing psr/http-client-implementation and psr/http-factory-implementation. To use Guzzle 7, for example, simply require guzzlehttp/guzzle:

composer require guzzlehttp/guzzle:^7.3

See HTTP Clients if you want to use your own HTTP client or use specific configuration options.

Getting started

You can simply create an instance of DocumentClient and use it in your class. Alternatively, you can create a repository.

use Swis\JsonApi\Client\DocumentClient;

$client = DocumentClient::create();
$document = $client->get('https://cms.contentacms.io/api/recipes');

/** @var \Swis\JsonApi\Client\Collection&\Swis\JsonApi\Client\Item[] $collection */
$collection = $document->getData();

foreach ($collection as $item) {
  // Do stuff with the items
}

Items

By default, all items are an instance of \Swis\JsonApi\Client\Item. The Item provides a Laravel Eloquent-like base class.

You can define your own models by extending \Swis\JsonApi\Client\Item or by implementing the \Swis\JsonApi\Client\Interfaces\ItemInterface yourself. This can be useful if you want to define, for example, hidden attributes, casts or get/set mutators. If you use custom models, you must register them with the TypeMapper.

Relations

This package implements Laravel Eloquent-like relations. These relations provide a fluent interface to retrieve the related items. There are currently four relations available:

  • HasOneRelation
  • HasManyRelation
  • MorphToRelation
  • MorphToManyRelation

Please see the following example about defining the relationships:

use Swis\JsonApi\Client\Item;

class AuthorItem extends Item
{
    protected $type = 'author';

    public function blogs()
    {
        return $this->hasMany(BlogItem::class);
    }
}

class BlogItem extends Item
{
    protected $type = 'blog';

    public function author()
    {
        return $this->hasOne(AuthorItem::class);
    }
}

Naming support

Relations should be defined using camelCase methods. Related items can then be accessed via magic attributes in camelCase or snake_case or by using the explicit name you used when defining the relation.

Collections

This package uses Laravel Collections as a wrapper for item arrays.

Links

All objects that can have links (i.e. document, error, item and relationship) use Concerns/HasLinks and thus have a getLinks method that returns an instance of Links. This is a simple array-like object with key-value pairs which are in turn an instance of Link or null.

Example

Given the following JSON:

{
	"links": {
		"self": "http://example.com/articles"
	},
	"data": [{
		"type": "articles",
		"id": "1",
		"attributes": {
			"title": "JSON:API paints my bikeshed!"
		},
		"relationships": {
			"author": {
				"data": {
					"type": "people",
					"id": "9"
				},
				"links": {
					"self": "http://example.com/articles/1/author"
				}
			}
		},
		"links": {
			"self": "http://example.com/articles/1"
		}
	}]
}

You can get the links this way:

/** @var $document \Swis\JsonApi\Client\Document */

// Document links
$links = $document->getLinks();
echo $links->self->getHref(); // http://example.com/articles

// Item links
$links = $document->getData()->getLinks();
echo $links->self->getHref(); // http://example.com/articles/1

// Relationship links
$links = $document->getData()->author()->getLinks();
echo $links->self->getHref(); // http://example.com/articles/1/author

Meta

All objects that can have meta information (i.e. document, error, item, jsonapi, link and relationship) use Concerns/HasMeta and thus have a getMeta method that returns an instance of Meta. This is a simple array-like object with key-value pairs.

Example

Given the following JSON:

{
	"links": {
		"self": {
			"href": "http://example.com/articles/1",
			"meta": {
				"foo": "bar"
			}
		}
	},
	"data": {
		"type": "articles",
		"id": "1",
		"attributes": {
			"title": "JSON:API paints my bikeshed!"
		},
		"relationships": {
			"author": {
				"data": {
					"type": "people",
					"id": "9"
				},
				"meta": {
					"written_at": "2019-07-16T13:47:26"
				}
			}
		},
		"meta": {
			"copyright": "Copyright 2015 Example Corp."
		}
	},
	"meta": {
		"request_id": "a77ab2b4-7132-4782-8b5e-d94ebaff6e13"
	}
}

You can get the meta this way:

/** @var $document \Swis\JsonApi\Client\Document */

// Document meta
$meta = $document->getMeta();
echo $meta->request_id; // a77ab2b4-7132-4782-8b5e-d94ebaff6e13

// Link meta
$meta = $document->getLinks()->self->getMeta();
echo $meta->foo; // bar

// Item meta
$meta = $document->getData()->getMeta();
echo $meta->copyright; // Copyright 2015 Example Corp.

// Relationship meta
$meta = $document->getData()->author()->getMeta();
echo $meta->written_at; // 2019-07-16T13:47:26

TypeMapper

All custom models must be registered with the TypeMapper. This TypeMapper maps, as the name suggests, JSON:API types to custom items.

Repository

For convenience, this package includes a basic repository with several methods to work with resources. You can create a repository for each of the endpoints you use based on \Swis\JsonApi\Client\Repository. This repository then uses standard CRUD endpoints for all its actions.

class BlogRepository extends \Swis\JsonApi\Client\Repository
{
    protected $endpoint = 'blogs';
}

The above repository will have a method for all CRUD-actions. If you work with a read-only API and don't want to have all actions, you can build your own repository by extending \Swis\JsonApi\Client\BaseRepository and including just the actions/traits you need.

use Swis\JsonApi\Client\Actions\FetchMany;
use Swis\JsonApi\Client\Actions\FetchOne;

class BlogRepository extends \Swis\JsonApi\Client\BaseRepository
{
    use FetchMany;
    use FetchOne;
    
    protected $endpoint = 'blogs';
}

If this repository (pattern) doesn't fit your needs, you can create your own implementation using the clients provided by this package.

Request parameters

All methods provided by the repository take extra parameters that will be appended to the url. This can be used, among other things, to add include and/or pagination parameters:

$repository = new BlogRepository();
$repository->all(['include' => 'author', 'page' => ['limit' => 15, 'offset' => 0]]);

ItemHydrator

The ItemHydrator can be used to fill/hydrate an item and its relations using an associative array with attributes. This is useful if you would like to hydrate an item with POST data from your request:

$typeMapper = new TypeMapper();
$itemHydrator = new ItemHydrator($typeMapper);
$blogRepository = new BlogRepository(DocumentClient::create($typeMapper), new DocumentFactory());

$item = $itemHydrator->hydrate(
    $typeMapper->getMapping('blog'),
    request()->all(['title', 'author', 'date', 'content', 'tags']),
    request()->id
);
$blogRepository->save($item);

Relations

The ItemHydrator also hydrates (nested) relations. A relation must explicitly be listed on the item in the $availableRelations array in order to be hydrated. If we take the above example, we can use the following attributes array to hydrate a new blog item:

$attributes = [
    'title'   => 'Introduction to JSON:API',
    'author'  => [
        'id'       => 'f1a775ef-9407-40ba-93ff-7bd737888dc6',
        'name'     => 'Björn Brala',
        'homepage' => 'https://github.com/bbrala',
    ],
    'co-author' => null,
    'date'    => '2018-12-02 15:26:32',
    'content' => 'JSON:API was originally drafted in May 2013 by Yehuda Katz...',
    'media' => [],
    'tags'    => [
        1,
        15,
        56,
    ],
];
$itemDocument = $itemHydrator->hydrate($typeMapper->getMapping('blog'), $attributes);

echo json_encode($itemDocument, JSON_PRETTY_PRINT);

{
    "data": {
        "type": "blog",
        "attributes": {
            "title": "Introduction to JSON:API",
            "date": "2018-12-02 15:26:32",
            "content": "JSON:API was originally drafted in May 2013 by Yehuda Katz..."
        },
        "relationships": {
            "author": {
                "data": {
                    "type": "author",
                    "id": "f1a775ef-9407-40ba-93ff-7bd737888dc6"
                }
            },
            "co-author": {
                "data": null
            },
            "media": {
                "data": []
            },
            "tags": {
                "data": [{
                    "type": "tag",
                    "id": "1"
                }, {
                    "type": "tag",
                    "id": "15"
                }, {
                    "type": "tag",
                    "id": "56"
                }]
            }
        }
    },
    "included": [{
        "type": "author",
        "id": "f1a775ef-9407-40ba-93ff-7bd737888dc6",
        "attributes": {
            "name": "Björn Brala",
            "homepage": "https://github.com/bbrala"
        }
    }]
}

As you can see in this example, relations can be hydrated by id, or by an associative array with an id and more attributes. If the item is hydrated using an associative array, it will be included in the resulting json unless setOmitIncluded(true) is called on the relation. You can unset a relation by passing null for singular relations or an empty array for plural relations.

N.B. Morph relations require a 'type' attribute to be present in the data in order to know which type of item should be created.

Handling errors

A request can fail due to several reasons and how this is handled depends on what happened. If the DocumentClient encounters an error there are basically three options.

Non 2xx request without body

If a response does not have a successful status code (2xx) and does not have a body, the DocumentClient (and therefore also the Repository) will return an instance of InvalidResponseDocument.

Non 2xx request with invalid JSON:API body

If a response does not have a successful status code (2xx) and does have a body, it is parsed as if it's a JSON:API document. If the response can not be parsed as such document, a ValidationException will be thrown.

Non 2xx request with valid JSON:API body

If a response does not have a successful status code (2xx) and does have a body, it is parsed as if it's a JSON:API document. In this case the DocumentClient (and therefore also the Repository) will return an instance of Document. This document contains the errors from the response, assuming the server responded with errors.

Checking for errors

Based on the above rules you can check for errors like this:

$document = $repository->all();

if ($document instanceof InvalidResponseDocument || $document->hasErrors()) {
    // do something with errors
}

Clients

This package offers two clients; DocumentClient and Client.

DocumentClient

This is the client that you would generally use e.g. the repository uses this client internally. Per the JSON:API spec, all requests and responses are documents. Therefore, this client always expects a \Swis\JsonApi\Client\Interfaces\DocumentInterface as input when posting data and always returns this same interface. This can be a plain Document when there is no data, an ItemDocument for an item, a CollectionDocument for a collection or an InvalidResponseDocument when the server responds with a non 2xx response.

The DocumentClient follows the following steps internally:

  1. Send the request using your HTTP client;
  2. Use ResponseParser to parse and validate the response;
  3. Create the correct document instance;
  4. Hydrate every item by using the item model registered with the TypeMapper or a \Swis\JsonApi\Client\Item as fallback;
  5. Hydrate all relationships;
  6. Add meta data to the document such as errors, links and meta.

Client

This client is a more low level client and can be used, for example, for posting binary data such as images. It can take everything your request factory takes as input data and returns the 'raw' \Psr\Http\Message\ResponseInterface. It does not parse or validate the response or hydrate items!

DocumentFactory

The DocumentClient requires ItemDocumentInterface instances when creating or updating resources. Such documents can easily be created using the DocumentFactory by giving it a DataInterface instance. This can be an ItemInterface, usually created by the ItemHydrator, or a Collection.

HTTP Clients

By default the Client uses php-http/discovery to find an available HTTP client, request factory and stream factory so you don't have to setup those yourself. You can also specify your own HTTP client, request factory or stream factory. This is a perfect way to add extra options to your HTTP client or register a mock HTTP client for your tests:

if (app()->environment('testing')) {
    $httpClient = new \Swis\Http\Fixture\Client(
        new \Swis\Http\Fixture\ResponseBuilder('/path/to/fixtures')
    );
} else {
    $httpClient = new \GuzzleHttp\Client(
        [
            'http_errors' => false,
            'timeout' => 2,
        ]
    );
}

$typeMapper = new TypeMapper();
$client = DocumentClient::create($typeMapper, $httpClient);
$document = $client->get('https://cms.contentacms.io/api/recipes');

N.B. This example uses our swisnl/php-http-fixture-client when in testing environment. This package allows you to easily mock requests with static fixtures. Definitely worth a try!

Advanced usage

If you don't like to use the supplied repository or clients, you can also parse a 'raw' \Psr\Http\Message\ResponseInterface or a simple json string using the Parsers\ResponseParser or Parser\DocumentParser respectively.

Change log

Please see CHANGELOG for more information on what has changed recently.

Testing

composer test

Contributing

Please see CONTRIBUTING and CODE_OF_CONDUCT for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

SWIS ❤️ Open Source

SWIS is a web agency from Leiden, the Netherlands. We love working with open source software.

json-api-client's People

Contributors

bbrala avatar burningdog avatar fivell avatar jazo avatar karinarastsinskagia avatar rapkis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

json-api-client's Issues

post and patch are not working in master branch

post and patch are not working in master branch

performing any post or patch request json-api-client fails with

TypeError: Argument 1 passed to Swis\JsonApi\Client\DocumentClient::sanitizeJson() must be of the type string, array given, called in .../vendor/swisnl/json-api-client/src/DocumentClient.php on line 103 

please check also my comment here 5715478#r31782624

HasOne relations hydrated by id using ItemHydrator are not correctly serialized

Detailed description

$itemHydrator = new ItemHydrator(new TypeMapper());

$item = $itemHydrator->hydrate(new ItemWithHasOneRelation(), ['hasone' => 1]);

$item->toJsonApiArray();

Result:

[
    'type' => 'foo-bar',
    'relationships' => [
        'hasone' => [
            'data' => null
        ]
    ]
]

Expected:

[
    'type' => 'foo-bar',
    'relationships' => [
        'hasone' => [
            'data' => [
                'type' => 'foo-baz',
                'id' => 1
            ]
        ]
    ]
]

Feature request: Split the Repository into traits

Detailed description

Our repositories don't always include all the CRUD actions.
e.g. we have read-only resources that only include the GET index and GET singular resource endpoints.
If I'd extend Swis\JsonApi\Client\Repository our repositories would have methods that call non-existing endpoints.
To prevent this I have created custom repositories that basically have a selection of the Swis\JsonApi\Client\Repository methods.

Possible implementation

The Swis\JsonApi\Client\Repository class consists of all the CRUD methods.
If these methods would be moved to traits and the Repository would use those, the class would work the same.
It would also enable custom repositories with just a few of these traits, without having to rewrite them.
We could then make repositories ourselves using your traits like hasIndexAction and hasDeleteAction.

Drop Laravel <6 support

Detailed description

Drop Laravel <6 support as those versions are not maintained anymore.

Unable to fetch resource with empty to-one relationship

When resource have an empty to-one relationship, the package unable to parse the document.

Detailed description

I believe this is a bug. I tried to fetch resource which had empty to-one relationship defined. But the package unable to parse the document because it has null object given when it expect a class which implement ItemInterface. For the record, i'm using custom model with TypeMapper.

Argument 1 passed to Swis\JsonApi\Client\Parsers\DocumentParser::getItemKey() must implement interface Swis\JsonApi\Client\Interfaces\ItemInterface, null given, called in C:\Users\afrastgeek\Projects\Playground\blog-composer\vendor\swisnl\json-api-client\src\Parsers\DocumentParser.php on line 213

However, empty relationship are recognized in the specification.

Sample resource i tried to fetch:

{
    data: {
        type: "cars",
        id: "1",
        attributes: {
            brand: "Voluptatum ab perspiciatis asperiores eos.",
            color: "Sapiente est libero cumque corporis voluptate odio.",
            created-at: 1567417355,
            updated-at: 1567417355
        },
        relationships: {
            user: {
                data: null,
                links: {
                    self: "http://blog-api.localhost/api/v1/cars/1/relationships/user",
                    related: "http://blog-api.localhost/api/v1/cars/1/user"
                }
            },
            garages: {
                data: [
                    {
                        type: "garages",
                        id: "1"
                    }
                ],
                links: {
                    self: "http://blog-api.localhost/api/v1/cars/1/relationships/garages",
                    related: "http://blog-api.localhost/api/v1/cars/1/garages"
                }
            }
        },
        links: {
            self: "http://blog-api.localhost/api/v1/cars/1"
        }
    }
}

Environment

  • Version used: Apache 2.4.39, PHP 7.3.8
  • Operating system and version: Windows 10
  • Laravel 5.8.34
  • swisnl/json-api-client 1.0.0-beta

Replace jenssegers/model dependency

Detailed description

Replace jenssegers/model dependency.

Context

jenssegers/model doesn't receive much love from its maintainer. Besides, it requires illuminate/support and efforts to remove this dependency are ignored; jenssegers/model#37

Possible implementation

Pull the class into this package and maintain it ourselves.

Add missing documentation

Taken from #30 (comment)

  • How to setup http client

See https://github.com/swisnl/json-api-client#bind-clients.

See https://github.com/swisnl/json-api-client#itemhydrator.

See https://github.com/swisnl/json-api-client#request-parameters for fetching included items.
See https://github.com/swisnl/json-api-client#itemhydrator for posting included items.

  • Recommended way and full example to test client classes mocking HTTP requests

See https://github.com/swisnl/json-api-client#bind-clients and https://github.com/swisnl/php-http-fixture-client.

  • Support for singular resources without id e.g. GET /profile

The take method can be used for this.

  • Top level meta vs resource level meta

See https://github.com/swisnl/json-api-client#meta

  • How to paginate records

See https://github.com/swisnl/json-api-client#request-parameters.

  • How to setup TypeMapper or Hydrator to work with current classes

See https://github.com/swisnl/json-api-client#typemapper

See https://github.com/swisnl/json-api-client#handling-errors

  • Complex objects (how to create resource if one of the attributes or several are custom complex objects and have specific json serialization/deseralization logic)

You can setup get/set mutators (see https://github.com/jenssegers/model#example) or use a class that implements \JsonSerializable (only for serialization).

  • Different naming support (camelcase/snakecase/dasherize, etc)

See https://github.com/swisnl/json-api-client#naming-support

  • What is responsibility of each class in json-api-client library

Most "public" classes are covered in the readme and the rest is self explanatory

LinksParser: Handle and debug error response when bad params are passed in filters / fields ecc.

When I pass a bad / not allowed param to the repo for retrieve, LinksParser always return a

Error links object MUST contain at least one of the following properties: about

statement, reporting no further information about the bad parameter which generated that error.
In my case, I passed a bad filter operator, %3E%3D instead of >= and I must halt LinksParser before line 53 to get what actually is in $data variable, as the Exception did not provide any useful further info to investigate to.

Dumping $data variable, it contains a stdClass object with only the via and info properties.
Pasting via->href link in postman, it finally shows the json:api response which had the errors object which finally explains verbose info about the error (in that case, a 400 with detailed explanation).

IMHO The errors response object from json:api should be included in the about property, in $data variable of LinkParser when throwing an Exception

Laravel 5.7 Support

Does not seem to support Laravel 5.7?

composer require swisnl/json-api-client
Using version ^0.10.3 for swisnl/json-api-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for swisnl/json-api-client ^0.10.3 -> satisfiable by swisnl/json-api-client[0.10.3].
    - swisnl/json-api-client 0.10.3 requires php-http/client-implementation ^1.0 -> no matching package found.


Potential causes:
 - A typo in the package name
 - The package is not available in a stable-enough version according to your minimum-stability setting
   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
 - It's a private package and you forgot to add a custom repository to find it

Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.

Installation failed, reverting ./composer.json to its original content.

401 responses don't result in an exception

We had an integration that failed because of an expired api key.
The error we had however wasn't useful at all because the json-api-client simply returned an empty collection.
So our script failed when it tried to use that data.

We would have expected that HTTP errors like 401, 404 and others would result in a PHP Exception.
The reason that this doesn't happen is because of this line:

return $this->client->sendRequest($this->buildRequest($method, $endpoint, $body, $headers));

The Guzzle Client sendRequest() method has hardcoded $options[RequestOptions::HTTP_ERRORS] = false;

https://github.com/guzzle/guzzle/blob/e3ff079b22820c2029d4c2a87796b6a0b8716ad8/src/Client.php#L131-L138

Possible implementation

I would suggest to change Client.php line 160 like this

-return $this->client->sendRequest($this->buildRequest($method, $endpoint, $body, $headers));
+return $this->client->send($this->buildRequest($method, $endpoint, $body, $headers));

Feature request: ability to send headers dynamically through repositories

Detailed description

I've been using the package for a while and it's great! The repositories are a huge help to implement things quickly. However, if I need to send any dynamic headers through the repositories, I can not use them, as their methods don't accept any arguments for headers. Therefore, it's then required to re-build the DocumentClient class, which has the feature, discussed in this issue: #33

Instead of re-building the class instance, we could reuse it by forwarding headers from the Repository classes. This would improve the developer experience, for example, when using the Laravel framework. Example below.

Context

I'm currently implementing an API that has multiple "Projects" within it. Each project uses a different access token defined in the headers. Alternatively, think of it as accessing multiple different accounts. My app may need to send requests to multiple projects within the same process, which requires changing the headers. This currently isn't possible and I'm either forced to skip the Repository pattern/feature completely or I'd have to override their methods to accept the header argument. Either way, the code isn't "DRY" anymore.

Alternatively, some APIs may require other types of dynamic headers, like unique request IDs, and so on. I think this is a low-effort feature since it only requires adding a single argument, but saves a lot of time when implementing such APIs. I'd be willing to open a PR for this if such a feature is accepted.

Possible implementation

Accept a new array $headers = [] argument for every method used in repositories (default to an empty array for backward compatibility). Then forward those arguments to the underlying DocumentClient class, which already accepts them.

Your environment

Include as many relevant details about the environment you experienced the bug in and how to reproduce it.

  • Package version used 2.3.2
  • I'm using it in a Laravel app, which leans heavily on dependency injection. With repositories, you can simply inject them once. But since they need different headers, I need to re-create a Client and a DocumentClient class, or have my own traits

json-api-client breaks serialization when working with associations

Detailed description

Given I have Items
PostItem and CategoryItem
PostItem has_one Category
I want to patch Post and unassign category relationship

$post = new PostItem();
$post->setId($postId);
$post->category()->associate(null);
$post->save(); // uses repository under the hood

I expect such payload to be generated by json-api-client

{"data":{"id":"1","type":"posts","relationships":{"category":{"data": null},"attributes":{}}} 

Actual behaviour

{"data":{"id":"1","type":"posts","relationships":{"category":{"data":{"type":"categories","id":null}}},"attributes":{"category_id": null}}}

so 1 issue here
attributes are populated with category_id value , but post doesn't have it.

also 1 notice

category relashionship data can have {"data": null } object instead of {"data":{"type":"categories","id":null}

possible bug is in this line https://github.com/swisnl/json-api-client/blame/7a0005837dc49c1723b8a1cd57a98653198bf531/src/Relations/HasOneRelation.php#L37

I do not know why such implementation exists but seems it is not by jsonapi spec

json-api-client doesn't support relationships without data section

Detailed description

While declaring item class developer should declare all the relations
if at least one of the relationships is skipped or added later by server

after more deep investigation it was discovered that JsonApiClient doesn't work by specification

According to spec https://jsonapi.org/format/#document-resource-object-relationships

Relationship should contain at least 1 of links, data, meta

So relationship with only links or meta considered to be valid by spec, but JsonApiClient doesn't work if there is no data section

client will fail with

Fatal error: Uncaught Art4\JsonApiClient\Exception\AccessException: "data" doesn't exist in Relationship. in .../vendor/art4/json-api-client/src/Relationship.php on line 119

Context

Developer should have ability to skip / not use not important relationships while developing

Possible implementation

json-api-client should check if relationship exist inside specific item class and skip building relationships instead of crashing.

Hydrator.php

$data = $relationship->get('data');

// patched here
          if(!$relationship->has('data')) {
            continue;
          }
          // patched here

Your environment

PHP 7.1
json-api-client from master (forked from https://github.com/didww/json-api-client/tree/Fivell-patch-1)

ParamNameMismatch

The class Item uses a trait src/Concerns/HasRelations.php and implements an interface src/Interfaces/ItemInterface.php.
the parameters name in method setRelation of HasRelation dont match with method setRelation of ItemInterface.

You might read in https://psalm.dev/docs/running_psalm/issues/ParamNameMismatch/ about it.

I suggest to rename parameters` name in method setRelation in classes which implements ItemInterface.

Using the library outside Laravel

Is it possible to use the library in a framework agnostic way? I am building a package which consumes a json api, and I'd love to take advantage of this package, but it is not clear if it requiere laravel or can be used without it.

If the anwser is true, is there some examples? Thank you so much!

Use illuminate/collections and split Laravel parts to json-api-client-laravel

Detailed description

Laravel collections illuminate/collections have been split into a separate package. Besides that we only use some array and string helpers in the framework agnostic parts of this package. We can extract the Laravel parts to a new package called json-api-client-laravel and drop the illuminate/support dependency.

Context

It is considered a bad practice to use illuminate/support in a framework agnostic package due to it's heavy weight and dependencies: https://mattallan.me/posts/dont-use-illuminate-support/. Besides that it will allow us to make it more clear how to use it with or without Laravel.

Possible implementation

This will obviously be a breaking change and should be considered for a 2.0 release.

Links-only relations not handled correctly

Detailed description

If you retrieve an item that has links-only relations, modify it and then save it - the JSON sent to the remote server includes the relations with an empty data member - even though the client doesn't know what the data content of that relationship is.

Here's an example to illustrate this. Given this code:

$repo = app(\App\Repositories\PostRepository::class);
$post = $repo->find(1)->getData();
$json = $post->toJsonApiArray();

Say the server replied to the find request with this:

{
  "data": {
    "type": "posts",
    "id": "1",
    "attributes": {
      "content": "This is my first blog.",
      "title": "Hello World!"
    },
    "relationships": {
      "comments": {
        "links": {
          "self": "http://localhost/api/v1/posts/1/relationships/comments",
          "related": "http://localhost/api/v1/posts/1/comments"
        }
      }
    },
    "links": {
      "self": "http://localhost/api/v1/posts/1"
    }
  }
}

I.e. the comments relationship has no data member.

When toJsonApiArray() is called, it will serialize this:

[
    'type' => 'posts',
    'id' => '1',
    'attributes' => [
        'content' => 'This is my first blog.',
        'title' => 'Hello World!'
    ],
    'relationships' => [
        'comments' => [
            'data' => [],
        ],
    ],
    'links' => [
        'self' => 'http://localhost/api/v1/posts/1'
    ],
]

I.e. the comments relationship now has an empty data member. This is incorrect because the data member was never retrieved from the remote server, so the client has no way of knowing what the value for the relationship is.

Presumably the same bug will occur if the server replied with a relationship that only had a meta member.

Context

The JSON API spec says that a relationship must have at least one of data, links, meta - so the client should handle relationships that do not have the data member. It should not assume the relationship has an empty data member for a relationship for which it has not been provided a data member.

In my scenario, changing an attribute on the post and saving it back to the server causes the request to be rejected, as the server does not accept a data member for the comments relationship.

Possible implementation

When serializing relationships, particularly when saving the resource, any relationships that do not have a data member should be skipped, rather than the relationship being assumed to be empty.

Your environment

Laravel 7.22, PHP 7.2, latest version of this package.

missed ability to handle invalid GET requests

Detailed description

Hello guys take a look at following implementation
https://github.com/swisnl/json-api-client/blob/master/src/Repository.php#L44
and
https://github.com/swisnl/json-api-client/blob/master/src/Repository.php#L84

For some reason Repository class raises an exception that doesn't allow to handle error document errors

By json.api spec get request can return error documents

for instance

https://jsonapi.org/format/#fetching-includes

If an endpoint does not support the include parameter, it MUST respond with 400 Bad Request to any requests that include it.

same for invalid filters, sorting etc...

I think repository class should be changed to raise Exception that has public property of Error instance that has description describes that include for such relation is not supported or filter is not valid or something else that server is responding.

To summarize:
Would be good to have ability to see user friendly error message for 400 responses that actually are valid jsonapi document errors, implemented by https://github.com/swisnl/json-api-client/blob/master/src/Errors/Error.php

2.0 plan

This meta issue is overview of the work for the 2.0 release.

Goals

We want to minimize the dependencies of this package. There are a few that are currently added that are not really needed. Also we want to make this package more framework agnotic and extract the Laravel code to a separate package.

Related issues

Milestone: https://github.com/swisnl/json-api-client/milestone/1

#85 - We will drop nyholm/psr7 when guzzle/psr7#327 is resolved so the consumer can just choose their PSR-17 factory.

#83 - Remove https://github.com/jenssegers/model dependency. Since the functionality it supplies is pretty simple we will pull maintenance to this package.

#80 - Currently we use illuminate/support (also through jessegers/model also this dependency). It is now possible to use illuminate/collections and have only the parts that are needed instead of full Laravel.

Laravel 5.6 Support

As of right now it looks like this works up to Laravel 5.5. When trying to use 5.6, getting an error about the version of support being used (see below). Any plans on upping the version requirement on that?

Using version ^0.8.0 for swisnl/json-api-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for swisnl/json-api-client ^0.8.0 -> satisfiable by swisnl/json-api-client[0.8.0].
    - Conclusion: remove laravel/framework v5.6.34
    - Conclusion: don't install laravel/framework v5.6.34
    - swisnl/json-api-client 0.8.0 requires illuminate/support 5.3.*|5.4.*|5.5.* -> satisfiable by laravel/framework[5.4.x-dev, 5.5.x-dev], illuminate/support[5.3.x-dev, 5.4.x-dev, 5.5.x-dev, v5.3.0, v5.3.16, v5.3.23, v5.3.4, v5.4.0, v5.4.13, v5.4.17, v5.4.19, v5.4.27, v5.4.36, v5.4.9, v5.5.0, v5.5.16, v5.5.17, v5.5.2, v5.5.28, v5.5.33, v5.5.34, v5.5.35, v5.5.36, v5.5.37, v5.5.39, v5.5.40, v5.5.41].
    - Can only install one of: laravel/framework[5.4.x-dev, v5.6.34].
    - Can only install one of: laravel/framework[5.5.x-dev, v5.6.34].
    - illuminate/support 5.3.x-dev conflicts with laravel/framework[v5.6.34].
    - illuminate/support 5.4.x-dev conflicts with laravel/framework[v5.6.34].
    - illuminate/support 5.5.x-dev conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.3.0 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.3.16 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.3.23 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.3.4 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.0 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.13 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.17 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.19 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.27 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.36 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.4.9 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.0 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.16 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.17 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.2 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.28 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.35 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.36 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.37 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.39 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.40 conflicts with laravel/framework[v5.6.34].
    - illuminate/support v5.5.41 conflicts with laravel/framework[v5.6.34].
    - don't install illuminate/support v5.5.33|don't install laravel/framework v5.6.34
    - don't install illuminate/support v5.5.34|don't install laravel/framework v5.6.34
    - Installation request for laravel/framework (locked at v5.6.34, required as 5.6.*) -> satisfiable by laravel/framework[v5.6.34].

How to set a relation to "null"?

Hi @JaZo ! Just don't figure how to set a relation to null when saving a content which had a previous relation set.
Relation "publisher" is a hasOne relation

This is the attributes/relations array prior to hydration. this array works as expected.

$ordineAttributes = [
                    note => $datiForm['note'],
                    'totale_ordine' => $datiForm['totale_ordine'],
                    'stato' => [
                        'id' => base64_decode($datiForm['stato'])
                    ],

/* THIS IS THE RELATION I WANT TO EVENTUALLY SET TO NULL (OR SOMEWHAT SIMILAR) */
                    'publisher' => [
                         'id' => $datiForm['publisher']
                    ],

                    'cliente' => [
                        'id' => $datiForm['cliente_id']
                    ],
                    'righe_ordine' => $ids,
                ];

$itemOrdineDocument = $itemHydrator->hydrate(
                        $typeMapper->getMapping('node--ordini'), 
                        $ordineAttributes,
                        $orderID
                ); 

I already tried to:

  • pass an empty publisher array
  • pass publisher => null
  • pass "id" => "none"
  • pass "id" => "null"
  • pass "id" => null
  • pass "id" => 0

Of course, if I completely remove the publisher entry from the ordineAttributes array, the hydrator steps over and leaves the previous value unchanged.

What I'm doing wrong?

I also get errors when I do

$itemOrdineDocument->unsetRelation('publisher');
$ordineRepo->save($itemOrdineDocument);

How retrieve types mapped in TypeMapperServiceProvider?

Hi Guys, this is driving me crazy.

    /**
     * A list of class names implementing \Swis\JsonApi\Client\Interfaces\ItemInterface.
     *
     * @var string[]
     */
    protected $items = [
        CampagneItem::class,
        OrdiniItem::class,
        GruppoOfferteItem::class,
        ClientiItem::class,
        AnagraficaClientiItem::class,
        UserItem::class,
        FormOfferteItem::class,
        OrderStatusItem::class
    ];

    /**
     * @param \Swis\JsonApi\Client\Interfaces\TypeMapperInterface $typeMapper
     */
    public function boot(TypeMapperInterface $typeMapper) {

        foreach ($this->items as $class) {
            /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface $item */
            $item = $this->app->make($class);


            $typeMapper->setMapping($item->getType(), $class);
        }
    }
}

in my custom controller how can i access to these mapped classes?
if i write $typeMapper = app(TypeMapper::class) is empty of course...
Sorry if question is stupid but I don't have noone to ask to...

jsonSerialize dont serialize "included"

Detailed description

If we get data from funcion return $this->client->get($this->endpoint.'?'.http_build_query($parameters)); ant then json encode, it didnt return included parameters

Make it clear if the issue is a bug, an enhancement or just a question.

BUG

Why is this change important to you? How would you use it?

If i get data from json api and want to return to Frontent i should do some extra steps

Possible implementation

File CollectionDocument.php

Add extra line:

if ($this->getIncluded()) {
            $document['included'] = $this->included->toArray();
        }

Not obligatory, but suggest an idea for implementing addition or change.

Your environment

Include as many relevant details about the environment you experienced the bug in and how to reproduce it.

  • Version used (e.g. PHP 7.2, HHVM 3):
  • Operating system and version (e.g. Ubuntu 16.04, Windows 7):
  • Link to your project:
  • ...
  • ...

Getting back NULL data from Ghost CMS API even though the request is succeeding.

I am trying to fetch data from Ghost CMS API. The document is coming back with no errors, but empty. That is, $blogs = $document->getData() returns NULL, even though a raw request returns a body. It seems to me that this is happening because the data is coming back in an array with key 'pages' or 'blogs' (not a 'data'=>[ ] one )

Detailed description

As above, I am firing a request, I get some data back - for instance the metadata with pagination information etc, but getData() yields NULL. I suspect that's because the Ghost API doesn't follow the "standard," in other words the data is not wrapped in a ['data'=>['...']] array.

The way I fire the request is as follows (I do pass some extra params in the Service Provider, e.g. AUTH):

use App\Api\v1\Repositories\PostRepository;

$repository = app(PostRepository::class);

$document = $repository->all();

dump($blogs->getResponse());    // YIELDS NULL

  dump($document->getMeta());  // YIELDS actual pages number, limit, etc.

This is a question

Is there a way of accessing the raw data? Your MD file says so, but I can't get my head round it.

parse a 'raw' \Psr\Http\Message\ResponseInterface ?

Context

I am trying to create "documents" and/or items by consuming the Ghost CMS API.

How can it benefit other users?

It might be useful when data returned needs fiddling with before it can be consumed.

Possible implementation

No suggestion

Your environment

PHP 7.4
Laravel 7.6.2
Docker Ghost 3.2 instance
Working on mac with Docker..

The data returned from ghost looks like this:

{
    "pages": [
        {
            "id": "5ebc205fabd4eb000100581c",
            "uuid": "b603a9a1-6938-4083-8a1b-a279ff846a29",
            "title": "Bespoke Solutions",
            "slug": "bespoke-solutions",
            "html": "<p>But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. </p><p>Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure? </p><p>On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee.</p>",
            "comment_id": "5ebc205fabd4eb000100581c",
            "feature_image": null,
            "featured": false,
            "visibility": "public",
            "created_at": "2020-05-13T16:29:19.000+00:00",
            "updated_at": "2020-05-13T16:29:39.000+00:00",
            "published_at": "2020-05-13T16:29:39.000+00:00",
            "custom_excerpt": null,
            "codeinjection_head": null,
            "codeinjection_foot": null,
            "custom_template": null,
            "canonical_url": null,
            "url": "http://localhost:2368/bespoke-solutions/",
            "excerpt": "But I must explain to you how all this mistaken idea of denouncing pleasure and\npraising pain was born and I will give you a complete account of the system, and\nexpound the actual teachings of the great explorer of the truth, the\nmaster-builder of human happiness. No one rejects, dislikes, or avoids pleasure\nitself, because it is pleasure, but because those who do not know how to pursue\npleasure rationally encounter consequences that are extremely painful. \n\nNor again is there anyone who loves o",
            "reading_time": 1,
            "page": true,
            "og_image": null,
            "og_title": null,
            "og_description": null,
            "twitter_image": null,
            "twitter_title": null,
            "twitter_description": null,
            "meta_title": null,
            "meta_description": null
        }
  ]
}

Children of repository are only `Item`s?

Detailed description

I have one repository CongressRepository and two Items, CongressItem & SpeakerItem. A CongressItem has many SpeakerItems.

If I fetch the children of CongressRepository, then they seem to be of Item class and not a class of CongressItem.

I am not sure if this is a bug or if I am just not using it right.

    $repository = app(App\Repositories\CongressRepository::class);
    $document = $repository->all();
    if ($document->hasErrors()) {
        // do something with errors
    } else {
        $congresses = $document->getData();
        $congress = $congresses->first(); // <- this is  an instance of `Item`, not of `CongressItem` 
       $congress->speakers()->getLinks() // <- This fails

The last command fails with this error message:

Symfony\Component\Debug\Exception\FatalThrowableError
Call to undefined method Swis\JsonApi\Client\Item::speakers()

This also causes other issues, like if only the link of speaker is provided by the API, then $congress->speakers is null.

Context

This is the output when calling the api /api/v1/congresses:

grafik

This is my CongressItem class:


<?php

namespace App\Items;

use Swis\JsonApi\Client\Item;

class CongressItem extends Item
{
    protected $type = 'congress';

    protected $availableRelations = ['speakers'];

    public function speakers()
    {
        return $this->hasMany(SpeakerItem::class);
    }
}

And my CongressRepository

<?php

namespace App\Repositories;

use Swis\JsonApi\Client\Repository;

class CongressRepository extends Repository
{
    protected $endpoint = 'congresses';
}

What I tried so far

I also tried to bind the TypeMapperServiceProvider and explicitly added the items there:

    protected $items = [
        \App\Items\CongressItem::class,
        \App\Items\SpeakerItem::class,
    ];

but this didn't help either. Also, as far as I understood the docs, I don't have to do this step because I am extending the class from Item.

Your environment

Using Laravel v6.4.1 on Ubuntu 18.04 with Homestead v9.4.0 & Vagrant 2.2.5. PHP Version is 7.3

ability set dynamic headers per request

Detailed description

From the source code it is not clear how to attach dynamic header for each request

Context

Different users should have ability to use they own API KEY to perform requests

Infinite loop toArray()

Context:
Monkey has a Banana. Banana has a Monkey.
Get request on Monkey with Banana included.
Monkey also gets loaded in Banana because of relationships but Monkey is not included with Banana.

Problem:
When you call toArray on Monkey's Banana and Banana calls toArray on Monkey, you end up with an unintended infinite loop.

Dirty fix:
$this->banana->setRelation('monkey', new JenssegersItem());

Recommended fix:
$this->banana->excludeRelation('monkey');

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.