JSON API transformer outputting valid (PSR-7) API Responses.
PHP
Latest commit 89204a8 Oct 11, 2016 @nilportugues committed on GitHub Merge pull request #83 from tybulewicz/master
Typo fix

README.md

JSON API Transformer & Server Helpers

Build Status Scrutinizer Code Quality SensioLabsInsight Latest Stable Version Total Downloads License Donate

Installation

Use Composer to install the package:

$ composer require nilportugues/json-api

Transfomer Classes

Given a PHP Object, and a series of mappings, the JSON API Transformer will represent the given data following the http://jsonapi.org specification.

For instance, given the following piece of code, defining a Blog Post and some comments:

$post = new Post(
  new PostId(9),
  'Hello World',
  'Your first post',
  new User(
      new UserId(1),
      'Post Author'
  ),
  [
      new Comment(
          new CommentId(1000),
          'Have no fear, sers, your king is safe.',
          new User(new UserId(2), 'Barristan Selmy'),
          [
              'created_at' => (new DateTime('2015/07/18 12:13:00'))->format('c'),
              'accepted_at' => (new DateTime('2015/07/19 00:00:00'))->format('c'),
          ]
      ),
  ]
);

And a Mapping series of classes implementing JsonApiMapping interface.

<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class PostMapping  implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \Post::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return 'Message';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [
            'author' => 'author',
            'title' => 'headline',
            'content' => 'body',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties() {
        return [ 
            'postId',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/posts/{postId}',
            'comments' => 'http://example.com/posts/{postId}/comments'
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getRelationships()
    {
        return [
            'author' => [ //this key must match with the property or alias of the same name in Post class.
                'related' => 'http://example.com/posts/{postId}/author',
                'self' => 'http://example.com/posts/{postId}/relationships/author',
            ]
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return ['author', 'title', 'body'];
    }
}
<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class PostIdMapping implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \PostId::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [],    
    }
    /**
     * {@inheritdoc}
     */    
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties()
        return [
            'postId',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/posts/{postId}',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getRelationships()
    {
        return [
            'comment' => [ //this key must match with the property or alias of the same name in PostId class.
                'self' => 'http://example.com/posts/{postId}/relationships/comments',
                ],
            ],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return [];
    }
}
<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class UserMapping implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \User::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties()
        return [
            'userId',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/users/{userId}',
            'friends' => 'http://example.com/users/{userId}/friends',
            'comments' => 'http://example.com/users/{userId}/comments',
        ];
    }    

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return [];
    }    
}
<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class UserIdMapping implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \UserId::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties()
        return ['userId'];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/users/{userId}',
            'friends' => 'http://example.com/users/{userId}/friends',
            'comments' => 'http://example.com/users/{userId}/comments',
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return [];
    }        
}
<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class CommentMapping implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \Comment::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties()
        return [ 'commentId',];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/comments/{commentId}',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getRelationships()
    {
        return [
            'post' => [ //this key must match with the property or alias of the same name in Comment class.
                'self' => 'http://example.com/posts/{postId}/relationships/comments',
            ]
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return [];
    }        
}
<?php
namespace AcmeProject\Infrastructure\Api\Mappings;

use NilPortugues\Api\Mappings\JsonApiMapping;

class CommentId implements JsonApiMapping
{
    /**
     * {@inhertidoc}
     */
    public function getClass() 
    {
        return \CommentId::class;
    }
    /**
     * {@inheritdoc}
     */
    public function getAlias()
    {
        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function getAliasedProperties() {
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getHideProperties(){
        return [];
    }
    /**
     * {@inheritdoc}
     */
    public function getIdProperties() {
        return [ 'commentId', ];
    }
    /**
     * {@inheritdoc}
     */
    public function getUrls()
    {
        return [
            'self' => 'http://example.com/comments/{commentId}',
        ];
    }
    /**
     * {@inheritdoc}
     */
    public function getRelationships()
    {
        return [
            'post' => [ //this key must match with the property or alias of the same name in CommentId class.
                'self' => 'http://example.com/posts/{postId}/relationships/comments',
            ]
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getRequiredProperties()
    {
        return [];
    }        
}

Calling the transformer will output a valid JSON API response using the correct formatting:

<?php

use NilPortugues\Api\JsonApi\JsonApiSerializer;
use NilPortugues\Api\JsonApi\JsonApiTransformer;
use NilPortugues\Api\JsonApi\Http\Message\Response;
use NilPortugues\Api\Mapping\Mapper;

$mappings = [
    \AcmeProject\Infrastructure\Api\Mappings\PostMapping::class,
    \AcmeProject\Infrastructure\Api\Mappings\PostIdMapping::class,
    \AcmeProject\Infrastructure\Api\Mappings\UserMapping::class,
    \AcmeProject\Infrastructure\Api\Mappings\UserIdMapping::class,
    \AcmeProject\Infrastructure\Api\Mappings\CommentMapping::class,
    \AcmeProject\Infrastructure\Api\Mappings\CommentId::class,
];

$mapper = new Mapper($mappings);

$transformer = new JsonApiTransformer($mapper);
$serializer = new JsonApiSerializer($transformer);

echo $serializer->serialize($post);

Output (formatted):

{
    "data": {
        "type": "message",
        "id": "9",
        "attributes": {
            "headline": "Hello World",
            "body": "Your first post"
        },
        "links": {
            "self": {
                "href": "http://example.com/posts/9"
            },
            "comments": {
                "href": "http://example.com/posts/9/comments"
            }
        },
        "relationships": {
            "author": {
                "links": {
                    "self": {
                        "href": "http://example.com/posts/9/relationships/author"
                    },
                    "related": {
                        "href": "http://example.com/posts/9/author"
                    }
                },
                "data": {
                    "type": "user",
                    "id": "1"
                }
            }
        }
    },
    "included": [
        {
            "type": "user",
            "id": "1",
            "attributes": {
                "name": "Post Author"
            },
            "links": {
                "self": {
                    "href": "http://example.com/users/1"
                },
                "friends": {
                    "href": "http://example.com/users/1/friends"
                },
                "comments": {
                    "href": "http://example.com/users/1/comments"
                }
            }
        },
        {
            "type": "user",
            "id": "2",
            "attributes": {
                "name": "Barristan Selmy"
            },
            "links": {
                "self": {
                    "href": "http://example.com/users/2"
                },
                "friends": {
                    "href": "http://example.com/users/2/friends"
                },
                "comments": {
                    "href": "http://example.com/users/2/comments"
                }
            }
        },
        {
            "type": "comment",
            "id": "1000",
            "attributes": {
                "dates": {
                    "created_at": "2015-08-13T21:11:07+02:00",
                    "accepted_at": "2015-08-13T21:46:07+02:00"
                },
                "comment": "Have no fear, sers, your king is safe."
            },
            "relationships": {
                "user": {
                    "data": {
                        "type": "user",
                        "id": "2"
                    }
                }
            },
            "links": {
                "self": {
                    "href": "http://example.com/comments/1000"
                }
            }
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

Server Classes

JSON API Request object

JSON API comes with its Request class, framework agnostic, implementing the PSR-7 Request Interface.

Using this request object will provide you access to all the interactions expected in a JSON API:

Defined Query Parameters:

  • &fields[resource]=field1,field2 will only show the specified fields for a given resource.
  • &include=resource show the relationship for a given resource.
  • &include=resource.resource2 show the relationship field for those depending on resource2.
  • &sort=field1,-field2 sort by field2 as DESC and field1 as ASC.
  • &sort=-field1,field2 sort by field1 as DESC and field2 as ASC.
  • &page[number] will return the current page elements in a page-based pagination strategy.
  • &page[size] will return the total amout of elements in a page-based pagination strategy.
  • &page[limit] will return the limit in a offset-based pagination strategy.
  • &page[offset] will return the offset value in a offset-based pagination strategy.
  • &page[cursor] will return the cursor value in a cursor-based pagination strategy.
  • &filter will return data passed in the filter param.

Request Object

Given the query parameters listed above, Request implements helper methods that parse and return data already prepared.

namespace \NilPortugues\Api\JsonApi\Http\Request;

class Request
{
  public function __construct(ServerRequestInterface $request = null) { ... }
  public function getIncludedRelationships() { ... }
  public function getSort() { ... }
  public function getPage() { ... }
  public function getFilters() { ... }
  public function getFields() { ... }
}

JSON API Response objects

Because the JSON API specification lists a set of behaviours, specific Response objects are provided for successful and error cases.

Success

  • NilPortugues\Api\JsonApi\Http\Response\Response
  • NilPortugues\Api\JsonApi\Http\Response\ResourceUpdated
  • NilPortugues\Api\JsonApi\Http\Response\ResourceAccepted
  • NilPortugues\Api\JsonApi\Http\Response\ResourceCreated
  • NilPortugues\Api\JsonApi\Http\Response\ResourceDeleted
  • NilPortugues\Api\JsonApi\Http\Response\ResourceProcessing

Error

  • NilPortugues\Api\JsonApi\Http\Response\BadRequest
  • NilPortugues\Api\JsonApi\Http\Response\ResourceConflicted
  • NilPortugues\Api\JsonApi\Http\Response\ResourceNotFound
  • NilPortugues\Api\JsonApi\Http\Response\TooManyRequests
  • NilPortugues\Api\JsonApi\Http\Response\UnprocessableEntity
  • NilPortugues\Api\JsonApi\Http\Response\UnsupportedAction

Forbidden Access

It is also possible to fire a Forbidden response by throwing the following Exception in your code:

  • NilPortugues\Api\JsonApi\Server\Actions\Exceptions\ForbiddenException

Access control logic is not provided.

Action Objects

Having Request and Response objects and Transformers, it just makes sense to have a set of classes that tie them all together into something more powerful: Actions.

Provided actions are:

  • NilPortugues\Api\JsonApi\Server\Actions\CreateResource
  • NilPortugues\Api\JsonApi\Server\Actions\DeleteResource
  • NilPortugues\Api\JsonApi\Server\Actions\GetResource
  • NilPortugues\Api\JsonApi\Server\Actions\ListResource
  • NilPortugues\Api\JsonApi\Server\Actions\PatchResource
  • NilPortugues\Api\JsonApi\Server\Actions\PutResource

All actions share a get method to run the Resource.

These get methods will expect in all cases one or more callables. This has been done to avoid coupling with any library or interface and being able to extend it.


Quality

To run the PHPUnit tests at the command line, go to the tests directory and issue phpunit.

This library attempts to comply with PSR-1, PSR-2, PSR-4 and PSR-7.

If you notice compliance oversights, please send a patch via Pull Request.

Contribute

Contributions to the package are always welcome!

Support

Get in touch with me using one of the following means:

Authors

License

The code base is licensed under the MIT license.