nilportugues/json-api

序列化转换器,输出有效的API响应,格式为JSON、JSON API和HAL+JSON API。


README

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

安装

使用 Composer 安装此包

$ composer require nilportugues/json-api

转换器类

给定一个PHP对象和一系列映射,JSON API 转换器将按照http://jsonapi.org规范表示给定的数据。

例如,给定以下代码片段,定义一篇博客文章和一些评论

$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'),
          ]
      ),
  ]
);

以及一系列实现JsonApiMapping接口的类。

<?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 [];
    }        
}

调用转换器将输出一个使用正确的格式的有效的JSON API响应

<?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);

输出(格式化)

{
    "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 请求对象

JSON API附带其请求类,框架无关,实现PSR-7请求接口。

使用此请求对象将为您提供访问JSON API中预期交互的方法。

定义的查询参数

  • &fields[resource]=field1,field2将只显示指定资源中的指定字段。
  • &include=resource显示给定资源的关联关系。
  • &include=resource.resource2显示依赖于resource2的关联字段。
  • &sort=field1,-field2按field2降序和field1升序排序。
  • &sort=-field1,field2按field1降序和field2升序排序。
  • &page[number]将在基于页面的分页策略中返回当前页面的元素。
  • &page[size]将在基于页面的分页策略中返回元素的总数。
  • &page[limit]将在基于偏移量的分页策略中返回限制数。
  • &page[offset]将在基于偏移量的分页策略中返回偏移量值。
  • &page[cursor]将在基于游标的分页策略中返回游标值。
  • &filter将返回通过过滤器参数传递的数据。

请求对象

给定上述查询参数,请求实现了解析并返回已准备数据的辅助方法。

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 响应对象

由于JSON API规范列出一组行为,因此为成功和错误情况提供了特定的响应对象。

成功

  • 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

错误

  • 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响应

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

未提供访问控制逻辑。

动作对象

有了请求、响应对象和转换器,自然应该有一组将它们全部结合起来以获得更强大功能的类:动作

提供以下动作

  • 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

所有操作都共享一个 get 方法来运行资源。

这些 get 方法在所有情况下都期望一个或多个 callables。这样做是为了避免与任何库或接口耦合,并能够扩展它。

质量

要在命令行运行PHPUnit测试,请转到测试目录并运行phpunit。

此库试图遵守PSR-1PSR-2PSR-4PSR-7

如果您发现遵守上的疏忽,请通过Pull Request发送补丁。

贡献

欢迎对包的贡献!

  • 请将您发现的任何错误或问题在问题跟踪器上报告。
  • 您可以在包的Git仓库中获取源代码。

支持

使用以下方式之一与我联系

作者

许可证

代码库采用MIT许可证授权。