nilportugues / php-json-api

JSON API transformer outputting valid (PSR-7) API Responses.
http://nilportugues.com
MIT License
71 stars 35 forks source link
api json json-api marshaller microservice microservices php php7 serialization serializer transformer

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:

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

Error

Forbidden Access

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

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:

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.