woohoolabs / yin
Woohoo Labs. Yin
Requires
- php: ^7.1.0||^8.0.0
- psr/http-message-implementation: ^1.0.0
Requires (Dev)
- justinrainbow/json-schema: ^5.2.0
- laminas/laminas-diactoros: ^2.0.0
- laminas/laminas-httphandlerrunner: ^1.0.0
- phpstan/phpstan: ^0.12.0
- phpstan/phpstan-phpunit: ^0.12.0
- phpstan/phpstan-strict-rules: ^0.12.0
- phpunit/phpunit: ^7.0.0||^8.2.0||^9.0.0
- seld/jsonlint: ^1.7.0
- squizlabs/php_codesniffer: ^3.5.1
- woohoolabs/coding-standard: ^1.1.2
- woohoolabs/releaser: ^1.1
Suggests
- justinrainbow/json-schema: Allows to validate the request and response body against the JSON API schema
- seld/jsonlint: Allows to lint the request and response body
- dev-master
- 5.0.x-dev
- 4.3.0
- 4.2.1
- 4.2.0
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.1
- 4.0.0
- 4.0.0-beta2
- 4.0.0-beta1
- 3.1.1
- 3.1.0
- 3.0.x-dev
- 3.0.2
- 3.0.1
- 3.0.0
- 3.0.0-beta1
- 2.0.6
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 2.0.0-rc1
- 2.0.0-beta2
- 2.0.0-beta1
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- 0.11.0
- 0.10.8
- 0.10.7
- 0.10.6
- 0.10.5
- 0.10.4
- 0.10.3
- 0.10.2
- 0.10.1
- 0.10.0
- 0.9.0
- 0.8.0
- 0.7.1
- 0.7.0
- 0.6.0
- 0.5.0
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.6
- 0.3.5
- 0.3.4
- 0.3.3
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.0
- 0.1.5
- 0.1.0
This package is auto-updated.
Last update: 2024-08-28 11:53:33 UTC
README
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
仅适用于特殊用例(例如,当文档可以包含多种类型的资源时),我们在这里不对其进行介绍。
AbstractSimpleResourceDocument
和 AbstractSingleResourceDocument
类之间的区别在于前者不需要 资源 对象。因此,对于真正简单的域对象(如消息),应首选前者,而对于更复杂的域对象(如用户或地址),后者工作得更好。
让我们先快速看看 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)
)。
AbstractSingleResourceDocument
和AbstractCollectionDocument
之间的唯一区别是它们看待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
:可用于两种类型的请求
为了简洁起见,我们只介绍后者的用法,因为它只是AbstractCreateHydrator
和AbstractUpdateHydrator
的并集。让我们看看一个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
参数在开发环境中可能很有用,因为你想在错误响应中返回触发异常的原始请求体。
为了验证当前请求的Accept
和Content-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. Harmony、Zend-Stratigility、Zend-Expressive或Slim 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-compose
和make
,只需运行以下命令即可尝试示例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的fields
和include
参数来限制检索的字段和关系。
书籍示例的示例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
安装的依赖项。
集成
- dimvic/yii-yin:Yii 1.1 集成
- paknahad/jsonapi-bundle:Symfony 集成
- qpautrat/woohoolabs-yin-bundle:Symfony 集成
版本控制
此库遵循 SemVer v2.0.0。
变更日志
有关最近更改的更多信息,请参阅 CHANGELOG。
测试
Woohoo Labs. Yin 包含一个 PHPUnit 测试套件。要从项目目录运行测试,请执行以下命令
$ phpunit
此外,您还可以运行 docker-compose up
或 make test
来执行测试。
贡献
有关详细信息,请参阅 CONTRIBUTING。
支持
有关详细信息,请参阅 SUPPORT。
鸣谢
许可证
MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件。