JSON API transformer outputting valid (PSR-7) API Responses.
MIT License
Use Composer to install the package:
$ composer require nilportugues/json-api
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"
}
}
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:
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() { ... }
}
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.
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.
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.
Contributions to the package are always welcome!
Get in touch with me using one of the following means:
The code base is licensed under the MIT license.