Giter VIP home page Giter VIP logo

json-api's Introduction

Build Status Scrutinizer Code Quality Code Coverage License

Description

JSON API logo

A good API is one of most effective ways to improve the experience for your clients. Standardized approaches for data formats and communication protocols increase productivity and make integration between applications smooth.

This framework agnostic package implements JSON API specification version v1.1 and helps focusing on core application functionality rather than on protocol implementation. It supports document structure, errors, data fetching as described in JSON API Format and covers parsing and checking HTTP request parameters and headers. For instance it helps to correctly respond with Unsupported Media Type (HTTP code 415) and Not Acceptable (HTTP code 406) to invalid requests. You don't need to manually validate all input parameters on every request. You can configure what parameters are supported by your services and this package will check incoming requests automatically. It greatly simplifies API development and fully support specification. In particular

  • Resource attributes and relationships
  • Polymorphic resource data and relationships
  • Compound documents with inclusion of related resources (circular resource references supported)
  • Meta information for document, resources, errors, relationship and link objects
  • Profiles
  • Parsing HTTP Accept and Content-Type headers in accordance with RFC 7231
  • Parsing HTTP query parameters (e.g. pagination, sorting and etc)
  • Sparse fieldsets and customized included paths
  • Errors

High code quality and 100% test coverage with 150+ tests. Production ready.

To find out more, please check out the Wiki and Sample App.

“I'm loving how easy it makes it to quickly implement an api”

Jeremy Cloutier

Full-stack Integration

This package is framework agnostic and if you are looking for practical usage sample you might be interested in Quick start JSON API server application Limoncello App.

The server supports

  • CRUD operations for a few sample data models and Users.
  • Cross-origin requests (CORS) to API server.
  • Authentication (Bearer token) and authorizations for CRUD operations.
  • Support for such JSON API features as resource inclusion, pagination and etc.

Demo app screen-shot

Sample usage

Assuming you've got an $author of type \Author you can encode it to JSON API as simple as this

$encoder = Encoder::instance([
        Author::class => AuthorSchema::class,
    ])
    ->withUrlPrefix('http://example.com/api/v1')
    ->withEncodeOptions(JSON_PRETTY_PRINT);

echo $encoder->encodeData($author) . PHP_EOL;

will output

{
    "data" : {
        "type"       : "people",
        "id"         : "123",
        "attributes" : {
            "first-name": "John",
            "last-name": "Doe"
        },
        "relationships" : {
            "comments" : {
                "links": {
                    "related" : "http://example.com/api/v1/people/123/comments"
                }
            }
        },
        "links" : {
            "self" : "http://example.com/api/v1/people/123"
        }
    }
}

The AuthorSchema provides information about resource's attributes and might look like

class AuthorSchema extends BaseSchema
{
    public function getType(): string
    {
        return 'people';
    }

    public function getId($author): ?string
    {
        return $author->authorId;
    }

    public function getAttributes($author, ContextInterface $context): iterable
    {
        return [
            'first-name' => $author->firstName,
            'last-name'  => $author->lastName,
        ];
    }

    public function getRelationships($author, ContextInterface $context): iterable
    {
        return [
            'comments' => [
                self::RELATIONSHIP_LINKS_SELF    => false,
                self::RELATIONSHIP_LINKS_RELATED => true,

                // Data include supported as well as other cool features
                // self::RELATIONSHIP_DATA => $author->comments,
            ],
        ];
    }
}

Parameter http://example.com/api/v1 is a URL prefix that will be applied to all encoded links unless they have a flag set telling not to add any prefixes.

Parameter JSON_PRETTY_PRINT is a PHP predefined JSON constant.

A sample program with encoding of multiple, nested, filtered objects and more is here.

For more advanced usage please check out the Wiki.

Versions

Current version is 4.x (PHP 7.1+) for older PHP (PHP 5.5 - 7.0, HHVM) please use version 1.x.

Questions?

Do not hesitate to check issues or post a new one.

Need help?

Are you planning to add JSON API and need help? We'd love to talk to you [email protected].

Contributing

If you have spotted any specification changes that are not reflected in this package please post an issue. Pull requests for documentation and code improvements are welcome.

There are 2 ways to send pull requests

  • small pull requests should be sent to develop branch as 1 commit
  • for bigger pull requests (e.g. new features) it's recommended to create an issue requesting a new branch for that feature. When a new branch named feature/issueXX is created (where XX is the issue number) you should post pull requests to this branch. When the feature is completed the branch will be squashed and merged to develop and then to master branches.

License

Apache License (Version 2.0). Please see License File for more information.

json-api's People

Contributors

boris-glumpler avatar bravo-kernel avatar efinder2 avatar fordarnold avatar gitter-badger avatar gregpeden avatar hglattergotz avatar lavatoaster avatar liamkarlmitchell avatar lindyhopchris avatar lunika avatar neomerx avatar pablokowalczyk avatar shadowhand 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

json-api's Issues

For deeply nested duplicates assert in ElementPresenter fails

in method setRelationshipTo assert('$parentExists === true') fails

way to reproduce

Encoder::instance([
            Author::class  => AuthorSchema::class,
            Comment::class => CommentSchema::class,
            Post::class    => PostSchema::class,
            Site::class    => SiteSchema::class,
        ], $this->encoderOptions)->encode([$this->site, $this->site]);

Author must have null relationship to Comments otherwise the error does no occures

Adding Links to resources and included

How can one add links to a resource object such as

{
    "data": {
        "type": "leads",
        "id": "29",
        "attributes": {
            "status": 0
        },
        "links": {
            "self": "localhost:4000\/leads\/29",
            "somelink": "link"
        }
    }
}

where "somelink" is the link to be added? Thanks!

Relationship without a data object, but only with links. Is this possible?

Hi,

I am trying to have a relationship that does not include the data object and only use the links. This way my API can async load them later.

This is what I tried, but my relationship does not have any value, just null:

public function getRelationships($product)
{
    return [
        'translations' => [
            self::SHOW_SELF => true,
            self::SHOW_RELATED => true
        ];
}

Is this possible and how would I accomplish this?

Filter nested attributes

Given the following attributes:

"attributes": {
    "username": "vivalacrowe",
    "private": {
        "email": "[email protected]",
        "name": "Rob"
    }
}

How do you filter to only get the email address. I've tried:

$params = new \Neomerx\JsonApi\Parameters\EncodingParameters(
    null,
    [
        'user' => ['private.email']
    ]
);

But this returns:

{
    "data": {
        "type": "user",
        "id": "12287",
        "links": {
            "self": "https:\/\/api.crowdcube.com\/users\/12287\/followers"
        }
    }
}

Filtering seems to only work on top level keys?

Revert exclusion of intermediate resources

It used to be that intermediate resources had to be excluded.

Now: Because compound documents require full linkage, intermediate resources in a multi-part path must be returned along with the leaf nodes. For example, a response to a request for comments.author should automatically include comments as well as the author of each of those comments.

However they should be excluded within relationships with 'aliases'. See related #16

Adding multiple errors

According to the wiki it's possible to do something like:

$error = new Error(
    'some-id',
    new Link('about-link'),
    'some-status',
    'some-code',
    'some-title',
    'some-detail',
    ['source' => 'data'],
    ['some'   => 'meta']
);

Encoder::instance([])->encodeError($error);

but it does not seem possible to add multiple errors. Or is it? If so what would be the way to do so?

My use case is the following:

  • A user sends a domain/ip address as part of an HTTP request (JSON encoded in the request body). I want to validate both parameters before proceeding further with the request. I want to return all possible errors (i.e, invalid domain and invalid ip address, etc).

Is this possible?

Relationships with single item arrays are converted to single object

If relationship data is array and the array contains only 1 item the array is converted into single object.

Instead of

{
    "data" : {
        "type"  : "posts",
        "id"    : "1",
        "attributes" : {
            "title" : "Title",
            "body"  : "Body"
        },
        "relationships" : {
            "comments" : {
                "data" : [
                    { "type":"comments", "id":"5" }
                ]
            }
        },
        "links" : {
            "self" : "http://example.com/posts/1"
        }
    }
}

The encoder returns (note: relationships --> comments --> data)

{
    "data" : {
        "type"  : "posts",
        "id"    : "1",
        "attributes" : {
            "title" : "Title",
            "body"  : "Body"
        },
        "relationships" : {
            "comments" : {
                "data" :  { "type":"comments", "id":"5" }
            }
        },
        "links" : {
            "self" : "http://example.com/posts/1"
        }
    }
}

Buggy enhancement #52

In limoncello-collins tests fail as it calls the render with $request and $exception as first two parameters (it's kinda interface for all renders). Render should be fixed accordingly.

Related to #52

Cursor pagination and attributes object

Hey there,

looking at various packages to implement an API conform to the JSON-API spec atm and it seems yours is the most complete one so far. Looking through the readme, the only things I noticed missing were cursor-based pagination and the attributes top-level object to hold information about the resource. Would you be interested in PRs for these things or is this maybe already in development? Cheers!

Easier encoding for paging links

That's an issue for discussion how to make pagination links generation easier.

As JSON-API spec says it is agnostic about the pagination strategy as there are at least a few possibilities e.g. page-based, offset-based, and cursor-based. For this reason pagination strategy is not detailed in the spec.

Currently paging links could be generated like this

$links = [
    Link::FIRST => new Link('/authors?page=1'),
    Link::LAST  => new Link('/authors?page=4'),
    Link::NEXT  => new Link('/authors?page=6'),
    Link::LAST  => new Link('/authors?page=9'),
];
$encoder->withLinks($links)->encodeData($authors);

While this approach is almost fine it would've better if Encoder looked into $authors and took sub-URL part /authors from AuthorSchema.

The question is what interface such a method could have to be flexible and easy to use? There are a few thinks that should be in mind: URLs might be relative as in the sample above or absolute http://example.com/api/authors?..., links might have meta information and different paging strategies are possible.

One of the possible approach

$encoder
    ->withPagingLinks([Link::FIRST, Link::LAST, ...], $pageLinksGenerator)
    ->encodeData(...);

Where first parameter is a list of desired links (all possible values are defined by the spec) and $pageLinksGenerator is an instance of a class which implements

interface PageLinksGeneratorInterface
{
    public function getLink(string $linkKey, string $schemaSubUrl);
}

So PageLinksGeneratorInterface implementation e.g. PageBasedLinksGenerator will do it for particular strategy and user can chose which one to use.

Support resource types that can behave as a proxy (e.x. Doctrine Proxies etc.)

Hi,

There are some use cases where the resource type that is defined with Encoder::instance() will not match the one that another schema is returning, but instead is a subclass. One of them is when you return a proxy object in the getRelationships method of a schema.

I will give a code example. Let's say we have a PostSchema that has the following relationships:

public function getRelationships($post)
{
    return [
        'author' => [
            self::DATA => $post->getAuthor() // will not return an Author object, but an AuthorProxy which extends Author
        ]
    ];
}

Now when you want to encode a Post, it will give you an error, saying that there is no provider for AuthorProxy. You see the problem? AuthorProxy IS an Author, but the encoder cannot handle this.

This is caused by Container::getSchema($resource):

public function getSchema($resource)
{
    $resourceType = get_class($resource);
    // ...
}

I can propose two solutions. One is that we check with instanceof if it can provide a schema for $resource. The second solution is that it supports Doctrine natively, we can do this by extending the Container with:

public function getSchema($resource)
{
    $resourceType = get_class($resource);

    // support for Doctrine Proxies
    if (class_exists('Doctrine\Common\Util\ClassUtils')) {
        $resourceType = ClassUtils::getRealClass($resourceType);
    }

    // ...
}

Also, it could have it's own ClassUtils class that will get the correct class name if there is __CG__ (common marker name for proxy classes) in the name. Then it will support Doctrine too and probably more libraries that implement the Proxy pattern.

Nested selfSubUrl

Is it possible to have a 'selfSubUrl' such as /users/{id}/followers.

Apologies if I missed it in the docs.

Github badge glitch

Github shows there is a problem with build when Travis says it's totally fine. Must be a caching issue.

PHP 5.5+ required?

Hi.

I am getting:

"FatalErrorException in Encoder.php line 145:
syntax error, unexpected 'class' (T_CLASS), expecting identifier (T_STRING) or variable
(T_VARIABLE) or '{' or '$'"

when I try to instantiate the Encoder using:

$encoder = Encoder::instance([
    '\My\Path\Models\MyModel' => '\My\Path\Schemas\MyModelSchema',
], new EncoderOptions(JSON_PRETTY_PRINT));

I am running PHP 5.4. Is "json-api" compatible with this version or do I need to upgrade to 5.5 please?

Thanks.

Relationships with empty data might have links and meta

Current implementation uses JSON API RC3 convention that relations with empty data ([] or null) are encoded as null or []. Since version 1.0 relationship can have links and meta with data set to null or [].

Thus instead of

{
    ...
    "relationships" : {
        "author" : null
    },
    ...
}

It should be

{
    ...
    "relationships" : {
        "author" : {"data" : null}
    },
    ...
}

The lib restricts links on resource level to 'self' only

spotted by @yoanisgil in #54

Not very often used feature however it should give options to add custom links on resource level

{
  "data": 
  {
    "type": "someType",
    "id": "123",
    "attributes": {
      "name": "some name"
    },
    "links": {
        "custom-link": "..." 
    }
  }
}

Links on 'relationships' level could be used while it's not added

Encoding Relationships

Hi,

Just wondering how relationship endpoints are meant to be encoded? The encoder interface doesn't seem to have a method for encoding relationships, and I can't find any mention in your wiki about it.

The example instance from the JSON-API spec would be:

GET /articles/1/relationships/author HTTP/1.1
Accept: application/vnd.api+json
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "links": {
    "self": "/articles/1/relationships/author",
    "related": "/articles/1/author"
  },
  "data": {
    "type": "people",
    "id": "12"
  }
}

If you have an associative array, the encoder will choke when trying to get the schema of the first element

Hi,

When you have an associative array somewhere in your $data array that you pass to the Encoder, the Parser will choke when trying to get the schema.

// Parser.php
if (is_array($data) === true) {
    $isOriginallyArrayed = true;
    $schema = $this->container->getSchema($data[0]); // better would be to use reset($data)
} else {
    $isOriginallyArrayed = false;
    $schema = $this->container->getSchema($data);
    $data   = [$data];
}

Encoder fails to encode empty data if field set is specified

Test snippet

$actual = $endcoder->encode([], null, null, new EncodingParameters(null, [
    // include only these attributes and links
    'authors' => [Author::ATTRIBUTE_FIRST_NAME, Author::LINK_COMMENTS],
]));

PHP Fatal error: Call to a member function getIncludePaths() on a non-object in /json-api/src/Encoder/Encoder.php on line 196

Add aliases support for relationships

From spec: A server MAY choose to expose a deeply nested relationship such as comments.author as a direct relationship with an alias such as comment-authors. This would allow a client to request /articles/1?include=comment-authors instead of /articles/1?include=comments.author. By abstracting the nested relationship with an alias, the server can still provide full linkage in compound documents without including potentially unwanted intermediate resources.

It used to be excluded by default for nested relationships (and it is implemented in RC3). Now this functionality has to be changed to work with aliases. See related #15

Semver

To follow semver, shouldn't the version numbers be more like 0.2.0, so that you can do something like ~0.2.0 in composer.json. Not that it matters much as long as the version isn't 1.0.

I'm guessing you're planning to tag v1.0.0 when the spec becomes stable?

Documentation Improvements: README

It's not clear from the examples in the README what exactly the instance method requires for input. A DocBlock or a standard function description including data types would be the minimum requirement here, but it appears that the inputs are complex enough to merit more thorough descriptions.

For example, in the following snippet:

$encoder = Encoder::instance([
    Author::class  => AuthorSchema::class,
], new JsonEncodeOptions(JSON_PRETTY_PRINT));

echo $encoder->encode($author) . PHP_EOL;

What exactly is "Author::class" or "AuthorSchema::class"? Is "::class" a class constant that was not defined in the code included in the "sample" directory? Or are you trying to demonstrate that the input format should map a model class with a schema class? Even after looking at the source code, I'm not entirely sure. Also, since the $author instantiation isn't shown, it's unclear what exactly it is.

A few more explanations here would go a long way and be much appreciated. Thanks.

Invalid page and filter params aren't validated

If you decide to add in a non-spec parameter to the request such as page=2, the application errors out. This should be handled and result in a 400 Bad Request instead.

The affected parameters I'm aware of are:

  • page
  • filter

This is the from the error log:

[2015-08-05 13:50:57] local.ERROR: exception 'ErrorException' with message 'Argument 6 passed to Neomerx\JsonApi\Factories\Factory::createParameters() must be of the type array, string given, called in /home/vagrant/www/work/lyf/vendor/neomerx/json-api/src/Parameters/ParametersParser.php on line 100 and defined' in /home/vagrant/www/work/lyf/vendor/neomerx/json-api/src/Factories/Factory.php:173
Stack trace:
#0 /home/vagrant/www/work/lyf/vendor/neomerx/json-api/src/Factories/Factory.php(173): Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(4096, 'Argument 6 pass...', '/home/vagrant/w...', 173, Array)
#1 /home/vagrant/www/work/lyf/vendor/neomerx/json-api/src/Parameters/ParametersParser.php(100): Neomerx\JsonApi\Factories\Factory->createParameters(Object(Neomerx\JsonApi\Parameters\Headers\Header), Object(Neomerx\JsonApi\Parameters\Headers\AcceptHeader), NULL, NULL, NULL, '2', NULL, NULL)
#2 /home/vagrant/www/work/lyf/vendor/neomerx/limoncello/src/Http/JsonApiTrait.php(305): Neomerx\JsonApi\Parameters\ParametersParser->parse(Object(Lyf\Http\Controllers\JsonApi\LaravelIntegration), Object(Neomerx\Limoncello\Errors\ExceptionThrower))
#3 /home/vagrant/www/work/lyf/vendor/neomerx/limoncello/src/Http/JsonApiTrait.php(316): Lyf\Http\Controllers\JsonApi\JsonApiController->getUncheckedParameters()
#4 /home/vagrant/www/work/lyf/vendor/neomerx/limoncello/src/Http/JsonApiTrait.php(334): Lyf\Http\Controllers\JsonApi\JsonApiController->checkParameters()
#5 /home/vagrant/www/work/lyf/vendor/neomerx/limoncello/src/Http/JsonApiTrait.php(398): Lyf\Http\Controllers\JsonApi\JsonApiController->getParameters()
#6 /home/vagrant/www/work/lyf/app/Http/Controllers/JsonApi/JsonApiController.php(46): Lyf\Http\Controllers\JsonApi\JsonApiController->getContentResponse(Array, 200, NULL, NULL)
#7 /home/vagrant/www/work/lyf/app/Http/Controllers/Api/UsersController.php(31): Lyf\Http\Controllers\JsonApi\JsonApiController->getResponse(Object(Illuminate\Database\Eloquent\Collection))
#8 [internal function]: Lyf\Http\Controllers\Api\UsersController->index()

Lazy load relationship data

I have the following code:

    public function getRelationships($user)
    {
        return [
            'followers' => [
                self::SHOW_DATA    => false,
                self::DATA         => function () use ($user) {
                    return $user->followers;
                },
                self::SHOW_RELATED => true,
            ]
        ];
    }

It would be nice if that closure wasn't evaluated until needed. In my case when include=followers in which case you would then call and render.

It seems like I have to potentially run multiple SQL statements even if the data isn't returned in the JSON.

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.