Woohoo Labs. Yin

4.3.0 2021-04-20 11:04 UTC

README

Latest Version on Packagist Software License Build Status Coverage Status Quality Score Total Downloads Gitter

Woohoo Labs. Yin 是一个 PHP 框架,它可以帮助您构建精美的 JSON:API。

目录

简介

JSON:API 规范于 2015 年 5 月 29 日达到 1.0 版本,我们认为这对 RESTful API 来说是一个重要的日子,因为这个规范可以帮助您构建更健壮和面向未来的 API。Woohoo Labs. Yin(以阴阳命名)旨在为您的 JSON:API 服务器带来效率和优雅,而Woohoo Labs. Yang 是其客户端对应物。

功能

  • 100% PSR-7 兼容性
  • 99% JSON:API 1.1 兼容性(大约)
  • 为效率和易用性而开发
  • 详尽的文档和示例
  • 提供文档和转换器以获取资源
  • 提供填充器以创建和更新资源
  • 附加中间件以简化启动和调试

为什么选择 Yin?

完整的 JSON:API 框架

Woohoo Labs. Yin 是一个与框架无关的库,它支持 JSON:API 1.1 规范的大部分内容:它提供包括内容协商、错误处理和分页、以及获取、创建、更新和删除资源在内的各种功能。尽管 Yin 由许多松散耦合的包和类组成,可以单独使用,但当作为整体使用时,该框架最为强大。

效率

我们设计 Yin 以尽可能高效。这就是为什么只有当请求时才转换属性和关系。当有很多资源需要转换或很少需要的转换非常昂贵时,这个特性非常有优势。此外,由于转换器是无状态的,避免了为每个资源单独创建模型对象的开销。此外,由于无状态性,整个库与依赖注入配合得非常好。

补充中间件

有一些附加中间件可能对 Woohoo Labs. Yin 有用。它可以简化各种任务,如错误处理(通过将异常转换为 JSON:API 错误响应)、分发了解 JSON:API 的控制器或调试(通过检查和验证请求和响应的语法)。

安装

开始之前,您需要Composer

安装 PSR-7 实现

因为 Yin 需要 PSR-7 实现(一个提供 psr/http-message-implementation 虚拟包的包),所以您必须先安装一个。您可以使用 Laminas Diactoros 或其他您偏好的任何库

$ composer require laminas/laminas-diactoros

安装 Yin

要安装此库的最新版本,请运行以下命令

$ composer require woohoolabs/yin

注意:默认情况下不会下载测试和示例。如果您需要它们,必须使用 composer require woohoolabs/yin --prefer-source 或克隆仓库。

Yin 的最新版本至少需要 PHP 7.1,但您可以使用 Yin 2.0.6 为 PHP 7.0。

安装可选依赖

如果您想利用请求/响应验证,则还必须请求以下依赖项

$ composer require justinrainbow/json-schema
$ composer require seld/jsonlint

基本用法

当使用 Woohoo Labs. Yin 时,您将创建

  • 文档和资源,以将域对象映射到 JSON:API 响应
  • hydrate,以便将 POST 或 PATCH 请求中的资源转换为域对象

此外,一个 JsonApi 类将负责仪器,而一个 PSR-7 兼容的 JsonApiRequest 类提供您通常需要的功能。

文档

以下各节将指导您创建成功响应的文档以及创建或构建错误文档。

成功响应的文档

对于成功请求,您必须返回有关一个或多个资源的信息。Woohoo Labs. Yin 提供了多个抽象类,帮助您为不同的用例创建自己的文档

  • AbstractSuccessfulDocument:一个用于成功响应的通用基础文档
  • AbstractSimpleResourceDocument:关于单个、非常简单的顶级资源的基类
  • AbstractSingleResourceDocument:关于单个、更复杂的顶级资源的基类
  • AbstractCollectionDocument:关于顶级资源集合的基类

由于 AbstractSuccessfulDocument 仅适用于特殊用例(例如,当文档可以包含多种类型的资源时),我们在这里不对其进行介绍。

AbstractSimpleResourceDocumentAbstractSingleResourceDocument 类之间的区别在于前者不需要 资源 对象。因此,对于真正简单的域对象(如消息),应首选前者,而对于更复杂的域对象(如用户或地址),后者工作得更好。

让我们先快速看看 AbstractSimpleResourceDocument:它有一个需要实现的抽象方法 getResource()。当您扩展此类时,需要实现此方法。`getResource()` 方法返回一个包含类型、id、属性和关系(如下所示)的整个转换后的资源数组

protected function getResource(): array
{
    return [
        "type"       => "<type>",
        "id"         => "<ID>",
        "attributes" => [
            "key" => "value",
        ],
    ];
}

请注意,`AbstractSimpleResourceDocument` 不支持一些开箱即用的功能,如稀疏字段集、自动包含相关资源等。这就是为什么这种文档类型应仅作为快速解决方案考虑,并且在大多数用例中,通常应选择下面介绍的更高级的文档类型。

`AbstractSingleResourceDocument` 和 `AbstractCollectionDocument` 都需要一个 `resource` 对象才能工作,这是在以下各节中介绍的概念。现在,只需知道必须在实例化文档时传递一个即可。这意味着您的文档的最小构造函数应如下所示

public function __construct(MyResource $resource)
{
    parent::__construct($resource);
}

当然,您可以为构造函数提供其他依赖项,或者如果您不需要它,完全可以省略它。

当您扩展 `AbstractSingleResourceDocument` 或 `AbstractCollectionDocument` 中的任何一个时,它们都要求您实现以下方法

/**
 * Provides information about the "jsonapi" member of the current document.
 *
 * The method returns a new JsonApiObject object if this member should be present or null
 * if it should be omitted from the response.
 */
public function getJsonApi(): ?JsonApiObject
{
    return new JsonApiObject("1.1");
}

描述非常明确:如果您想在响应中包含 jsonapi 成员,则创建一个新的 JsonApiObject。其构造函数期望 JSON:API 版本号和一个可选的元对象(作为数组)。

/**
 * Provides information about the "meta" member of the current document.
 *
 * The method returns an array of non-standard meta information about the document. If
 * this array is empty, the member won't appear in the response.
 */
public function getMeta(): array
{
    return [
        "page" => [
            "offset" => $this->object->getOffset(),
            "limit" => $this->object->getLimit(),
            "total" => $this->object->getCount(),
        ]
    ];
}

文档还可能包含一个“元”成员,可以包含任何非标准信息。上面的示例向文档中添加了关于分页的信息。

请注意,object属性是任何类型的变量(在这种情况下是一个假设的集合),这是文档的主要“主题”。

/**
 * Provides information about the "links" member of the current document.
 *
 * The method returns a new DocumentLinks object if you want to provide linkage data
 * for the document or null if the member should be omitted from the response.
 */
public function getLinks(): ?DocumentLinks
{
    return new DocumentLinks(
        "https://example.com/api",
        [
            "self" => new Link("/books/" . $this->getResourceId())
        ]
    );

    /* This is equivalent to the following:
    return DocumentLinks::createWithBaseUri(
        "https://example.com/api",
        [
            "self" => new Link("/books/" . $this->getResourceId())
        ]
    );
}

这次,我们想在文档中显示一个自链接。为此,我们使用getResourceId()方法,这是调用资源(下面将介绍)以获取主资源ID的快捷方式($this->resource->getId($this->object))。

AbstractSingleResourceDocumentAbstractCollectionDocument之间的唯一区别是它们看待object的方式。前者将其视为单个领域对象,而后者将其视为可迭代的集合。

用法

可以将文档转换为HTTP响应。实现这一点最简单的方法是使用JsonApi并选择适当响应类型。成功的文档支持三种类型的响应

  • 正常:响应中可以包含所有顶级成员(除了“错误”)
  • 元信息:只有“jsonapi”、“links”和元顶级成员可以出现在响应中
  • 关系:指定的关系对象将成为响应的主要数据

错误响应的文档

可以使用AbstractErrorDocument创建用于错误响应的可重用文档。它也需要实现与成功文档相同的抽象方法,但还可以使用addError()方法来包含错误项。

/** @var AbstractErrorDocument $errorDocument */
$errorDocument = new MyErrorDocument();
$errorDocument->addError(new MyError());

还有一个ErrorDocument,这使得可以即时构建错误响应

/** @var ErrorDocument $errorDocument */
$errorDocument = new ErrorDocument();
$errorDocument->setJsonApi(new JsonApiObject("1.0"));
$errorDocument->setLinks(ErrorLinks::createWithoutBaseUri()->setAbout("https://example.com/api/errors/404")));
$errorDocument->addError(new MyError());

资源

成功响应的文档可以包含一个或多个顶级资源和包含的资源。这就是为什么资源负责将领域对象转换为JSON:API资源和资源标识符。

虽然鼓励为每种资源类型创建一个转换器,但你也可以定义遵循组合设计模式的“组合”资源。

资源必须实现ResourceInterface。为了便于这项工作,你还可以扩展AbstractResource类。

AbstractResource类的子类需要实现几个抽象方法 - 其中大多数与在文档对象中看到的类似。以下示例演示了一个处理书籍领域对象及其“作者”和“出版社”关系的资源。

class BookResource extends AbstractResource
{
    /**
     * @var AuthorResource
     */
    private $authorResource;

    /**
     * @var PublisherResource
     */
    private $publisherResource;

    /**
     * You can type-hint the object property this way.
     * @var array
     */
    protected $object;

    public function __construct(
        AuthorResource $authorResource,
        PublisherResource $publisherResource
    ) {
        $this->authorResource = $authorResource;
        $this->publisherResource = $publisherResource;
    }

    /**
     * Provides information about the "type" member of the current resource.
     *
     * The method returns the type of the current resource.
     *
     * @param array $book
     */
    public function getType($book): string
    {
        return "book";
    }

    /**
     * Provides information about the "id" member of the current resource.
     *
     * The method returns the ID of the current resource which should be a UUID.
     *
     * @param array $book
     */
    public function getId($book): string
    {
        return $this->object["id"];

        // This is equivalent to the following (the $book parameter is used this time instead of $this->object):
        return $book["id"];
    }

    /**
     * Provides information about the "meta" member of the current resource.
     *
     * The method returns an array of non-standard meta information about the resource. If
     * this array is empty, the member won't appear in the response.
     *
     * @param array $book
     */
    public function getMeta($book): array
    {
        return [];
    }

    /**
     * Provides information about the "links" member of the current resource.
     *
     * The method returns a new ResourceLinks object if you want to provide linkage
     * data about the resource or null if it should be omitted from the response.
     *
     * @param array $book
     */
    public function getLinks($book): ?ResourceLinks
    {
        return new ResourceLinks::createWithoutBaseUri()->setSelf(new Link("/books/" . $this->getId($book)));

        // This is equivalent to the following:
        // return new ResourceLinks("", new Link("/books/" . $this->getResourceId()));
    }

    /**
     * Provides information about the "attributes" member of the current resource.
     *
     * The method returns an array where the keys signify the attribute names,
     * while the values are callables receiving the domain object as an argument,
     * and they should return the value of the corresponding attribute.
     *
     * @param array $book
     * @return callable[]
     */
    public function getAttributes($book): array
    {
        return [
            "title" => function () {
                return $this->object["title"];
            },
            "pages" => function () {
                return (int) $this->object["pages"];
            },
        ];

        // This is equivalent to the following (the $book parameter is used this time instead of $this->object):
        return [
            "title" => function (array $book) {
                return $book["title"];
            },
            "pages" => function (array $book) {
                return (int) $book["pages"];
            },
        ];
    }

    /**
     * Returns an array of relationship names which are included in the response by default.
     *
     * @param array $book
     */
    public function getDefaultIncludedRelationships($book): array
    {
        return ["authors"];
    }

    /**
     * Provides information about the "relationships" member of the current resource.
     *
     * The method returns an array where the keys signify the relationship names,
     * while the values are callables receiving the domain object as an argument,
     * and they should return a new relationship instance (to-one or to-many).
     *
     * @param array $book
     * @return callable[]
     */
    public function getRelationships($book): array
    {
        return [
            "authors" => function () {
                return ToManyRelationship::create()
                    ->setLinks(
                        RelationshipLinks::createWithoutBaseUri()->setSelf(new Link("/books/relationships/authors"))
                    )
                    ->setData($this->object["authors"], $this->authorTransformer);
            },
            "publisher" => function () {
                return ToOneRelationship::create()
                    ->setLinks(
                        RelationshipLinks::createWithoutBaseUri()->setSelf(new Link("/books/relationships/publisher"))
                    )
                    ->setData($this->object["publisher"], $this->publisherTransformer);
            },
        ];

        // This is equivalent to the following (the $book parameter is used this time instead of $this->object):

        return [
            "authors" => function (array $book) {
                return ToManyRelationship::create()
                    ->setLinks(
                        RelationshipLinks::createWithoutBaseUri()->setSelf(new Link("/books/relationships/authors"))
                    )
                    ->setData($book["authors"], $this->authorTransformer);
            },
            "publisher" => function ($book) {
                return ToOneRelationship::create()
                    ->setLinks(
                        RelationshipLinks::createWithoutBaseUri()->setSelf(new Link("/books/relationships/publisher"))
                    )
                    ->setData($book["publisher"], $this->publisherTransformer);
            },
        ];
    }
}

通常,您不会直接使用资源。只有文档需要它们来填充响应中的“数据”、“包含”和“关系”成员。

填充器

Hydrators允许我们根据当前HTTP请求初始化领域对象的属性。这意味着,当客户端想要创建或更新资源时,hydrators可以帮助实例化领域对象,然后可以进行验证、保存等操作。

Woohoo Labs中有三个抽象hydrator类。

  • AbstractCreateHydrator:可用于创建新资源的请求
  • AbstractUpdateHydrator:可用于更新现有资源的请求
  • AbstractHydrator:可用于两种类型的请求

为了简洁起见,我们只介绍后者的用法,因为它只是AbstractCreateHydratorAbstractUpdateHydrator的并集。让我们看看一个hydrator的示例

class BookHydrator extends AbstractHydrator
{
    /**
     * Determines which resource types can be accepted by the hydrator.
     *
     * The method should return an array of acceptable resource types. When such a resource is received for hydration
     * which can't be accepted (its type doesn't match the acceptable types of the hydrator), a ResourceTypeUnacceptable
     * exception will be raised.
     *
     * @return string[]
     */
    protected function getAcceptedTypes(): array
    {
        return ["book"];
    }

    /**
     * Validates a client-generated ID.
     *
     * If the $clientGeneratedId is not a valid ID for the domain object, then
     * the appropriate exception should be thrown: if it is not well-formed then
     * a ClientGeneratedIdNotSupported exception can be raised, if the ID already
     * exists then a ClientGeneratedIdAlreadyExists exception can be thrown.
     *
     * @throws ClientGeneratedIdNotSupported
     * @throws ClientGeneratedIdAlreadyExists
     * @throws Exception
     */
    protected function validateClientGeneratedId(
        string $clientGeneratedId,
        JsonApiRequestInterface $request,
        ExceptionFactoryInterface $exceptionFactory
    ) {
        if ($clientGeneratedId !== null) {
            throw $exceptionFactory->createClientGeneratedIdNotSupportedException($request, $clientGeneratedId);
        }
    }

    /**
     * Produces a new ID for the domain objects.
     *
     * UUID-s are preferred according to the JSON:API specification.
     */
    protected function generateId(): string
    {
        return Uuid::generate();
    }

    /**
     * Sets the given ID for the domain object.
     *
     * The method mutates the domain object and sets the given ID for it.
     * If it is an immutable object or an array the whole, updated domain
     * object can be returned.
     *
     * @param array $book
     * @return mixed|void
     */
    protected function setId($book, string $id)
    {
        $book["id"] = $id;

        return $book;
    }

    /**
     * You can validate the request.
     *
     * @throws JsonApiExceptionInterface
     */
    protected function validateRequest(JsonApiRequestInterface $request): void
    {
        // WARNING! THIS CONDITION CONTRADICTS TO THE SPEC
        if ($request->getAttribute("title") === null) {
            throw new LogicException("The 'title' attribute is required!");
        }
    }

    /**
     * Provides the attribute hydrators.
     *
     * The method returns an array of attribute hydrators, where a hydrator is a key-value pair:
     * the key is the specific attribute name which comes from the request and the value is a
     * callable which hydrates the given attribute.
     * These callables receive the domain object (which will be hydrated), the value of the
     * currently processed attribute, the "data" part of the request and the name of the attribute
     * to be hydrated as their arguments, and they should mutate the state of the domain object.
     * If it is an immutable object or an array (and passing by reference isn't used),
     * the callable should return the domain object.
     *
     * @param array $book
     * @return callable[]
     */
    protected function getAttributeHydrator($book): array
    {
        return [
            "title" => function (array $book, $attribute, $data, $attributeName) {
                $book["title"] = $attribute;

                return $book;
            },
            "pages" => function (array &$book, $attribute, $data, $attributeName) {
                $book["pages"] = $attribute;
            },
        ];
    }

    /**
     * Provides the relationship hydrators.
     *
     * The method returns an array of relationship hydrators, where a hydrator is a key-value pair:
     * the key is the specific relationship name which comes from the request and the value is a
     * callable which hydrate the previous relationship.
     * These callables receive the domain object (which will be hydrated), an object representing the
     * currently processed relationship (it can be a ToOneRelationship or a ToManyRelationship
     * object), the "data" part of the request and the relationship name as their arguments, and
     * they should mutate the state of the domain object.
     * If it is an immutable object or an array (and passing by reference isn't used),
     * the callable should return the domain object.
     *
     * @param mixed $book
     * @return callable[]
     */
    protected function getRelationshipHydrator($book): array
    {
        return [
            "authors" => function (array $book, ToManyRelationship $authors, $data, string $relationshipName) {
                $book["authors"] = BookRepository::getAuthors($authors->getResourceIdentifierIds());

                return $book;
            },
            "publisher" => function (array &$book, ToOneRelationship $publisher, $data, string $relationshipName) {
                $book["publisher"] = BookRepository::getPublisher($publisher->getResourceIdentifier()->getId());
            },
        ];
    }

    /**
     * You can validate the domain object after it has been hydrated from the request.
     * @param mixed $book
     */
    protected function validateDomainObject($book): void
    {
        if (empty($book["authors"])) {
            throw new LogicException("The 'authors' relationship cannot be empty!");
        }
    }
}

根据书籍示例,以下请求

POST /books HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "book",
    "attributes": {
      "title": "Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation",
      "pages": 512
    },
    "relationships": {
      "authors": {
        "data": [
            { "type": "author", "id": "100" },
            { "type": "author", "id": "101" }
        ]
      }
    }
  }
}

将导致以下Book领域对象

Array
(
    [id] => 1
    [title] => Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation
    [pages] => 512
    [authors] => Array
        (
            [0] => Array
                (
                    [id] => 100
                    [name] => Jez Humble
                )
            [1] => Array
                (
                    [id] => 101
                    [name] => David Farley
                )
        )
    [publisher] => Array
        (
            [id] => 12346
            [name] => Addison-Wesley Professional
        )
)

异常

Woohoo Labs. Yin 被设计得使错误处理尽可能简单和可定制。这就是为什么所有默认异常都扩展了 JsonApiException 类,并包含一个带有适当错误对象(s)的 错误文档。这就是为什么如果你想在异常情况下响应错误文档,你需要执行以下操作

try {
    // Do something which results in an exception
} catch (JsonApiExceptionInterface $e) {
    // Get the error document from the exception
    $errorDocument = $e->getErrorDocument();

    // Instantiate the responder - make sure to pass the correct dependencies to it
    $responder = Responder::create($request, $response, $exceptionFactory, $serializer);

    // Create a response from the error document
    $responder->genericError($errorDocument);

    // Emit the HTTP response
    sendResponse($response);
}

为了保证完全可定制,我们引入了 异常工厂 的概念。这些是创建 Woohoo Labs. Yin 抛出的所有异常的类。由于每个转换器和填充器都会传递一个 你自己的选择 的异常工厂,因此你可以完全自定义抛出的异常类型。

默认的 异常工厂 创建 JsonApiException 的子类,但你也可以自由创建任何 JsonApiExceptionInterface 异常。如果你只想自定义异常的错误文档或错误对象,只需扩展基本 Exception 类并创建你的 createErrorDocument()getErrors() 方法。

JsonApi

JsonApi 类是整个框架的协调者。如果你想要使用 Woohoo Labs. Yin 的全部功能,强烈建议使用这个类。你可以在 示例部分示例目录 中找到有关其使用的各种示例。

JsonApiRequest

JsonApiRequest 类实现了 WoohooLabs\Yin\JsonApi\Request\JsonApiRequestInterface,它扩展了 PSR-7 的 ServerRequestInterface 并添加了一些与 JSON:API 相关的有用方法。有关可用方法的更多信息,请参阅 JsonApiRequestInterface 的文档。

高级用法

本节将指导你了解 Yin 的高级功能。

分页

Yin 可以帮助你分页资源集合。首先,它提供了一些查询请求查询参数的快捷方式,当使用基于页面、基于偏移或基于游标的分页策略时。

基于页面的分页

Yin 查找 page[number]page[size] 查询参数并解析其值。如果它们中的任何一个缺失,则将使用默认的页面号或大小(以下示例中的 "1" 和 "10")。

$pagination = $jsonApi->getPaginationFactory()->createPageBasedPagination(1, 10);

固定的基于页面的分页

Yin 查找 page[number] 查询参数并解析其值。如果它缺失,则将使用默认的页面号(以下示例中的 "1")。此策略可以用于不希望完全公开页面大小的情况。

$pagination = $jsonApi->getPaginationFactory()->createFixedPageBasedPagination(1);

基于偏移的分页

Yin 查找 page[offset]page[limit] 查询参数并解析其值。如果它们中的任何一个缺失,则将使用默认的偏移或限制(以下示例中的 "1" 和 "10")。

$pagination = $jsonApi->getPaginationFactory()->createOffsetBasedPagination(1, 10);

基于游标的分页

Yin 查找 page[cursor]page[size] 查询参数并解析其值。如果它们中的任何一个缺失,则将使用默认的游标或大小(以下示例中的 "2016-10-01" 或 10)。

$pagination = $jsonApi->getPaginationFactory()->createCursorBasedPagination("2016-10-01", 10);

固定的基于游标的分页

Yin 查找 page[cursor] 查询参数并解析其值。如果它缺失,则将使用默认的游标(以下示例中的 "2016-10-01")。

$pagination = $jsonApi->getPaginationFactory()->createFixedCursorBasedPagination("2016-10-01");

自定义分页

如果你需要一个自定义的分页策略,你可以使用 JsonApiRequestInterface::getPagination() 方法,该方法返回一个分页参数数组。

$paginationParams = $jsonApi->getRequest()->getPagination();

$pagination = new CustomPagination($paginationParams["from"] ?? 1, $paginationParams["to"] ?? 1);

用法

一旦你有了适当的分页对象,你可以在从数据源获取数据时使用它们。

$users = UserRepository::getUsers($pagination->getPage(), $pagination->getSize());

分页链接

JSON:API规范允许提供资源集合的分页链接。Yin也能在这方面帮助你。当你为文档定义链接时,可以使用DocumentLinks::setPagination()方法。它期望一个分页URI和一个实现PaginationLinkProviderInterface的对象,如下面的示例所示。

public function getLinks(): ?DocumentLinks
{
    return DocumentLinks::createWithoutBaseUri()->setPagination("/users", $this->object);
}

为了让事情更简单,有一些LinkProvider特性可以简化内置分页策略的PaginationLinkProviderInterface实现。例如,针对User对象的集合可以使用PageBasedPaginationLinkProviderTrait。这样,只需要实现三个抽象方法。

class UserCollection implements PaginationLinkProviderInterface
{
    use PageBasedPaginationLinkProviderTrait;

    public function getTotalItems(): int
    {
        // ...
    }

    public function getPage(): int
    {
        // ...
    }

    public function getSize(): int
    {
        // ...
    }

    // ...
}

完整示例请见这里

高效地加载关系数据

有时调整关系的数据检索可能有益或必要。一个可能的场景是当你有一个包含无数项目的“多对多”关系时。如果这个关系并不是总是需要的,那么你可能只想在关系本身包含在响应中时返回关系的数据键。这种优化可以通过省略资源链接来节省带宽。

以下是一个从UserResource示例类中提取的示例。

public function getRelationships($user): array
{
    return [
        "contacts" => function (array $user) {
            return
                ToManyRelationship::create()
                    ->setData($user["contacts"], $this->contactTransformer)
                    ->omitDataWhenNotIncluded();
        },
    ];
}

通过使用omitDataWhenNotIncluded()方法,当关系不包括时将省略关系数据。然而,有时这种优化本身并不足够。尽管我们可以通过前面的技术来节省带宽,但由于我们使用setData()方法将关系传递给关系对象,因此关系仍然需要从数据源(可能是一个数据库)加载。

这个问题可以通过延迟加载关系来解决。要做到这一点,您必须使用setDataAsCallable()方法而不是setData()方法。

public function getRelationships($user): array
{
    return [
        "contacts" => function (array $user) {
            return
                ToManyRelationship::create()
                    ->setDataAsCallable(
                        function () use ($user) {
                            // Lazily load contacts from the data source
                            return $user->loadContactsFromDataSource();
                        },
                        $this->contactTransformer
                    )
                    ->omitDataWhenNotIncluded()
                ;
        },
    ];
}

这样,只有在给定的关系的data键存在于响应中时,用户的关系(如联系人)才会被加载,这使得您的API尽可能高效。

将元数据注入文档中

可以在运行时将元数据注入到文档中。如果你想要自定义或修饰你的响应,这会很有用。例如,如果你想在响应文档中注入一个缓存ID,你可以使用以下方法。

// Calculate the cache ID
$cacheId = calculateCacheId();

// Respond with "200 Ok" status code along with the book document containing the cache ID in the meta data
return $jsonApi->respond()->ok($document, $book, ["cache_id" => $cacheId]);

通常,每个响应器方法的最后一个参数可以用来向文档添加元数据。

内容协商

JSON:API标准指定了一些关于内容协商的规则。Woohoo Labs的Yin试图通过RequestValidator类帮助你强制执行这些规则。让我们首先创建一个请求验证器来看看它的作用。

$requestValidator = new RequestValidator(new DefaultExceptionFactory(), $includeOriginalMessageInResponse);

为了自定义可以抛出的异常,有必要提供一个异常工厂。另一方面,$includeOriginalMessageInResponse参数在开发环境中可能很有用,因为你想在错误响应中返回触发异常的原始请求体。

为了验证当前请求的AcceptContent-Type头部是否符合JSON:API规范,请使用此方法。

$requestValidator->negotiate($request);

请求/响应验证

你可以使用以下方法来检查当前请求的查询参数是否符合命名规则

$requestValidator->validateQueryParams($request);

注意:为了应用以下验证,请记住安装Yin的可选依赖

此外,可以验证请求体是否是一个格式良好的JSON文档。

$requestValidator->validateJsonBody($request);

同样,也可以验证响应。让我们首先创建一个响应验证器

$responseValidator = new ResponseValidator(
    new JsonSerializer(),
    new DefaultExceptionFactory(),
    $includeOriginalMessageInResponse
);

为了确保响应体是一个格式良好的JSON文档,可以使用以下方法

$responseValidator->validateJsonBody($response);

为了确保响应体是一个格式良好的JSON:API文档,可以使用以下方法

$responseValidator->validateJsonApiBody($response);

在开发环境中验证响应非常有用,可以早期发现可能的错误。

自定义序列化

您可以将Yin配置为以自定义方式序列化响应,而不是使用默认序列化器(JsonSerializer),该序列化器使用json_encode()函数将JSON:API文档写入响应体。

在大多数用例中,默认序列化器应该足以满足您的需求,但有时您可能需要更复杂的功能。或者有时您可能想做一些坏事,比如在没有序列化的情况下将您的JSON:API响应作为一个数组返回,假设您的API端点是“内部”调用的。

为了使用自定义序列化器,创建一个实现SerializerInterface的类,并相应地设置您的JsonApi实例(请注意最后一个参数)

$jsonApi = new JsonApi(new JsonApiRequest(), new Response(), new DefaultExceptionFactory(), new CustomSerializer());

自定义反序列化

您可以将Yin配置为以自定义方式反序列化请求,而不是使用默认的反序列化器(JsonDeserializer),该反序列化器使用json_decode()函数解析请求体的内容。

在大多数用例中,默认反序列化器应该足以满足您的需求,但有时您可能需要更复杂的功能。或者有时您可能想做一些坏事,比如在不将请求体转换为JSON格式的情况下,将您的JSON:API端点“内部”调用。

为了使用自定义反序列化器,创建一个实现DeserializerInterface的类,并相应地设置您的JsonApiRequest实例(请注意最后一个参数)

$request = new JsonApiRequest(ServerRequestFactory::fromGlobals(), new DefaultExceptionFactory(), new CustomDeserializer());

中间件

如果您使用的是面向中间件的框架(如Woohoo Labs. HarmonyZend-StratigilityZend-ExpressiveSlim Framework 3),您会发现Yin-middleware库非常有用。阅读文档以了解其优点!

示例

获取单个资源

public function getBook(JsonApi $jsonApi): ResponseInterface
{
    // Getting the "id" of the currently requested book
    $id = $jsonApi->getRequest()->getAttribute("id");

    // Retrieving a book domain object with an ID of $id
    $book = BookRepository::getBook($id);

    // Instantiating a book document
    $document = new BookDocument(
        new BookResource(
            new AuthorResource(),
            new PublisherResource()
        )
    );

    // Responding with "200 Ok" status code along with the book document
    return $jsonApi->respond()->ok($document, $book);
}

获取资源集合

public function getUsers(JsonApi $jsonApi): ResponseInterface
{
    // Extracting pagination information from the request, page = 1, size = 10 if it is missing
    $pagination = $jsonApi->getPaginationFactory()->createPageBasedPagination(1, 10);

    // Fetching a paginated collection of user domain objects
    $users = UserRepository::getUsers($pagination->getPage(), $pagination->getSize());

    // Instantiating a users document
    $document = new UsersDocument(new UserResource(new ContactResource()));

    // Responding with "200 Ok" status code along with the users document
    return $jsonApi->respond()->ok($document, $users);
}

获取关系

public function getBookRelationships(JsonApi $jsonApi): ResponseInterface
{
    // Getting the "id" of the currently requested book
    $id = $jsonApi->getRequest()->getAttribute("id");

    // Getting the currently requested relationship's name
    $relationshipName = $jsonApi->getRequest()->getAttribute("rel");

    // Retrieving a book domain object with an ID of $id
    $book = BookRepository::getBook($id);

    // Instantiating a book document
    $document = new BookDocument(
        new BookResource(
            new AuthorResource(),
            new PublisherResource(
                new RepresentativeResource()
            )
        )
    );

    // Responding with "200 Ok" status code along with the requested relationship document
    return $jsonApi->respond()->okWithRelationship($relationshipName, $document, $book);
}

创建新资源

public function createBook(JsonApi $jsonApi): ResponseInterface
{
    // Hydrating a new book domain object from the request
    $book = $jsonApi->hydrate(new BookHydrator(), []);

    // Saving the newly created book
    // ...

    // Creating the book document to be sent as the response
    $document = new BookDocument(
        new BookResource(
            new AuthorResource(),
            new PublisherResource(
                new RepresentativeResource()
            )
        )
    );

    // Responding with "201 Created" status code along with the book document
    return $jsonApi->respond()->created($document, $book);
}

更新资源

public function updateBook(JsonApi $jsonApi): ResponseInterface
{
    // Retrieving a book domain object with an ID of $id
    $id = $jsonApi->getRequest()->getResourceId();
    $book = BookRepository::getBook($id);

    // Hydrating the retrieved book domain object from the request
    $book = $jsonApi->hydrate(new BookHydrator(), $book);

    // Updating the book
    // ...

    // Instantiating the book document
    $document = new BookDocument(
        new BookResource(
            new AuthorResource(),
            new PublisherResource(
                new RepresentativeResource()
            )
        )
    );

    // Responding with "200 Ok" status code along with the book document
    return $jsonApi->respond()->ok($document, $book);
}

更新资源的关系

public function updateBookRelationship(JsonApi $jsonApi): ResponseInterface
{
    // Checking the name of the currently requested relationship
    $relationshipName = $jsonApi->getRequest()->getAttribute("rel");

    // Retrieving a book domain object with an ID of $id
    $id = $jsonApi->getRequest()->getAttribute("id");
    $book = BookRepository::getBook($id);
    if ($book === null) {
        die("A book with an ID of '$id' can't be found!");
    }

    // Hydrating the retrieved book domain object from the request
    $book = $jsonApi->hydrateRelationship($relationshipName, new BookHydrator(), $book);

    // Instantiating a book document
    $document = new BookDocument(
        new BookResource(
            new AuthorResource(),
            new PublisherResource(
                new RepresentativeResource()
            )
        )
    );

    // Responding with "200 Ok" status code along with the book document
    return $jsonApi->respond()->ok($document, $book);
}

如何尝试

如果您想了解Yin是如何工作的,请查看示例。如果您的系统上提供了docker-composemake,只需运行以下命令即可尝试示例API

cp .env.dist .env      # You can now edit the settings in the .env file
make composer-install  # Install the Composer dependencies
make up                # Start the webserver

最后,只需访问以下URL:localhost:8080。您甚至可以通过指定JSON:API的fieldsinclude参数来限制检索的字段和关系。

书籍示例的示例URI

  • GET /books/1:获取一本书
  • GET /books/1/relationships/authors:获取作者关系
  • GET /books/1/relationships/publisher:获取出版社关系
  • GET /books/1/authors:获取一本书的作者
  • POST /books:创建新书
  • PATCH /books/1:更新书
  • PATCH /books/1/relationships/author:更新书的作者
  • PATCH /books/1/relationships/publisher:更新书的出版社

用户示例的示例URI

  • GET /users:获取用户
  • GET /users/1:获取一个用户
  • GET /users/1/relationships/contacts:获取联系关系

完成工作后,只需停止web服务器

make down

如果您不具备先决条件,您必须设置web服务器,并在您的宿主系统上安装PHP以及通过Composer安装的依赖项。

集成

版本控制

此库遵循 SemVer v2.0.0

变更日志

有关最近更改的更多信息,请参阅 CHANGELOG

测试

Woohoo Labs. Yin 包含一个 PHPUnit 测试套件。要从项目目录运行测试,请执行以下命令

$ phpunit

此外,您还可以运行 docker-compose upmake test 来执行测试。

贡献

有关详细信息,请参阅 CONTRIBUTING

支持

有关详细信息,请参阅 SUPPORT

鸣谢

许可证

MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件