Woohoo Labs. Yang

3.0.0 2023-09-06 09:39 UTC

README

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

Woohoo Labs. Yang是一个PHP框架,可以帮助您更容易地与JSON:API服务器进行通信。

目录

简介

JSON:API规范在2015年5月29日达到了1.0版本,我们认为这也是RESTful API的一个重要日子,因为这个规范使得API比以往任何时候都更加健壮和面向未来。Woohoo Labs. Yang(以阴阳命名)旨在为您的JSON:API客户端带来效率和优雅,而Woohoo Labs. Yin则是其服务器端对应物。

功能

  • 100% PSR-7兼容性
  • 99% JSON:API 1.1符合度(大约)
  • 提供请求构建器,以便更容易地设置JSON:API请求
  • 通过PSR-18HTTPlug提供易于使用的HTTP客户端
  • 支持开箱即用的数据填充器,以便轻松将API响应转换为对象

安装

在开始之前,您只需要Composer

安装HTTP客户端和消息实现

由于Yang需要一个HTTP客户端实现,您必须先安装一个。您可以使用Guzzle 7适配器或任何其他您喜欢的库

$ composer require php-http/guzzle7-adapter

安装Yang

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

$ composer require woohoolabs/yang

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

Yang至少需要PHP 7.4。您可以使用Yang 2.3用于PHP 7.2。

基本用法

Yang以三种方式帮助您与JSON:API服务器通信。以下小节将涵盖这些主题。

请求构建器

Yang附带了一个强大的请求构建器,您可以使用它以JSON:API兼容的方式设置PSR-7 Request对象。为此,您可以使用如以下示例中所示的JsonApiRequestBuilder类。

use GuzzleHttp\Psr7\Request;
use WoohooLabs\Yang\JsonApi\Request\JsonApiRequestBuilder;

// Instantiate an empty PSR-7 request, note that the default HTTP method must be provided
$request = new Request('GET', '');

// Instantiate the request builder
$requestBuilder = new JsonApiRequestBuilder($request);

// Setup the request with general properties
$requestBuilder
    ->setProtocolVersion("1.1")
    ->setMethod("GET")
    ->setUri("https://www.example.com/api/users")
    ->setHeader("Accept-Charset", "utf-8");

// Setup the request with JSON:API specific properties
$requestBuilder
    ->setJsonApiFields(                                      // To define sparse fieldset
        [
            "users" => ["first_name", "last_name"],
            "address" => ["country", "city", "postal_code"]
        ]
    )
    ->setJsonApiIncludes(                                    // To include related resources
        ["address", "friends"]
    )
    ->setJsonApiIncludes(                                    // Or you can pass a string instead
        "address,friends"
    )
    ->setJsonApiSort(                                        // To sort resource collections
        ["last_name", "first_name"]
    )
    ->setJsonApiPage(                                        // To paginate the primary data
        ["number" => 1, "size" => 100]
    )
    ->setJsonApiFilter(                                      // To filter the primary data
        ["first_name" => "John"]
    )
    ->addJsonApiAppliedProfile(                              // To add a profile to the request (JSON:API 1.1 feature)
        ["https://example.com/profiles/last-modified"]
    )
    ->addJsonApiRequestedProfile(                            // To request the server to apply a profile (JSON:API 1.1 feature)
        ["https://example.com/profiles/last-modified"]
    )
    ->addJsonApiRequiredProfile(                             // To require the server to apply a profile (JSON:API 1.1 feature)
        ["https://example.com/profiles/last-modified"]
    );

// Setup the request body
$requestBuilder
    ->setJsonApiBody(                                        // You can pass the content as a JSON string
        '{
           "data": [
             { "type": "user", "id": "1" },
             { "type": "user", "id": "2" }
           ]
         }'
    )
    ->setJsonApiBody(                                        // or you can pass it as an array
        [
            "data" => [
                ["type" => "user", "id" => 1],
                ["type" => "user", "id" => 2],
            ],
        ]
    )
    ->setJsonApiBody(                                        // or as a ResourceObject instance
        new ResourceObject("user", 1)
    );

// Get the composed request
$request = $requestBuilder->getRequest();

如果您不想使用内置的请求构建器,您可以自由设置任何PSR-7 RequestInterface实例,以便继续下一步

$request = new Request('GET', '');
$request = $request
    ->withProtocolVersion("1.1")
    ->withUri(new Uri("https://example.com/api/users?fields[users]=first_name,last_name"))
    ->withHeader("Accept", "application/vnd.api+json")
    ->withHeader("Content-Type", "application/vnd.api+json");

HTTP客户端

该库支持PSR-18HTTPlug,因此您可以选择如何发送请求。如果您已安装php-http/guzzle6-adapter包,则可以使用Guzzle来完成此操作

use Http\Adapter\Guzzle6\Client;

// Instantiate the Guzzle HTTP Client
$guzzleClient = Client::createWithConfig([]);

// Instantiate the syncronous JSON:API Client
$client = new JsonApiClient($guzzleClient);

// Send the request syncronously to retrieve the response
$response = $client->sendRequest($request);

// Instantiate the asyncronous JSON:API Client
$client = new JsonApiAsyncClient($guzzleClient);

// Send the request asyncronously to retrieve a promise
$promise = $client->sendAsyncRequest($request);

// Send multiple request asyncronously to retrieve an array of promises
$promises = $client->sendConcurrentAsyncRequests([$request, $request]);

当然,您可以使用任何可用的HTTP客户端或通过PSR-18和HTTPlug创建自定义HTTP客户端。

响应

一旦您获取到服务器响应,就可以开始查询它。为了这个目的,Yang使用了PSR-7兼容的JsonApiResponse类。如果您使用了上面介绍过的HTTP客户端,则会自动得到这个类型的对象,否则您需要确保用正确的依赖项实例化它。

// Instantiate a JSON:API response object from a PSR-7 response object with the default deserializer
$response = new JsonApiResponse($psr7Response);

JsonApiResponse类——在PSR-7定义的类之上——提供了一些方法,使得处理JSON:API响应变得更加容易。

// Checks if the response doesn't contain any errors
$isSuccessful = $response->isSuccessful();

// Checks if the response doesn't contain any errors, and has the status codes listed below
$isSuccessful = $response->isSuccessful([200, 202]);

// The same as the isSuccessful() method, but also ensures the response contains a document
$isSuccessfulDocument = $response->isSuccessfulDocument();

// Checks if the response contains a JSON:API document
$hasDocument = $response->hasDocument();

// Retrieves and deserializes the JSON:API document in the response body
$document = $response->document();

Document类也有各种方法。

// Retrieves the "jsonapi" member as a JsonApiObject instance
$jsonApi = $document->jsonApi();

$jsonApiVersion = $jsonApi->version();
$jsonApiMeta = $jsonApi->meta();

// Checks if the document has the "meta" member
$hasMeta = $document->hasMeta();

// Retrieves the "meta" member as an array
$meta = $document->meta();

// Checks if the document has any links
$hasLinks = $document->hasLinks();

// Retrieves the "links" member as a DocumentLinks object
$links = $document->links();

// Checks if the document has any errors
$hasErrors = $document->hasErrors();

// Counts the number of errors in the document
$errorCount = $document->errorCount();

// Retrieves the "errors" member as an array of Error objects
$errors = $document->errors();

// Retrieves the first error as an Error object or throws an exception if it is missing
$firstError = $document->error(0);

// Checks if the document contains a single resource as its primary data
$isSingleResourceDocument = $document->isSingleResourceDocument();

// Checks if the document contains a collection of resources as its primary data
$isResourceCollectionDocument = $document->isResourceCollectionDocument();

// Checks if the document contains any primary data
$hasPrimaryData = $document->hasAnyPrimaryResources();

// Returns the primary resource as a ResourceObject instance if the document is a single-resource document
// or throws an exception otherwise or when the document is empty
$primaryResource = $document->primaryResource();

// Returns the primary resources as an array of ResourceObject instances if the document is a collection document
// or throws an exception otherwise
$primaryResources = $document->primaryResources();

// Checks if there are any included resources in the document
$hasIncludedResources = $document->hasAnyIncludedResources();

// Checks if there is a specific included resource in the document
$isUserIncluded = $document->hasIncludedResource("user", "1234");

// Retrieves all the included resources as an array of ResourceObject instances
$includedResources = $document->includedResources();

DocumentLinks类具有以下方法。

// Checks if the "self" link is present
$hasSelf = $links->hasSelf();

// Returns the "self" link as a Link object or throws an exception if it is missing
$selfLink = $links->self();

// Checks if the "related" link is present
$hasRelated = $links->hasRelated();

// Returns the "related" link as a Link object or throws an exception if it is missing
$relatedLink = $links->related();

// Checks if the "first" link is present
$hasFirst = $links->hasFirst();

// Returns the "first" link as a Link object or throws an exception if it is missing
$firstLink = $links->first();

// Checks if the "last" link is present
$hasLast = $links->hasLast();

// Returns the "last" link as a Link object or throws an exception if it is missing
$lastLink = $links->last();

// Checks if the "prev" link is present
$hasPrev = $links->hasPrev();

// Returns the "prev" link as a Link object or throws an exception if it is missing
$prevLink = $links->prev();

// Checks if the "next" link is present
$hasNext = $links->hasNext();

// Returns the "next" link as a Link object or throws an exception if it is missing
$nextLink = $links->next();

// Checks if a specific link is present
$hasLink = $links->hasLink("next");

// Returns a specific link as a Link object or throws an exception if it is missing
$link = $links->link("next");

// Checks if the there is any profile defined
$hasProfiles = $links->hasAnyProfiles();

// Retrieves the profiles as an array of ProfileLink objects
$profiles = $links->profiles();

// Checks if there is a specific profile defined
$hasProfile = $links->hasProfile("https://example.com/profiles/last-modified");

// Retrieves a specific profile as a ProfileLink object
$profile = $links->profile("https://example.com/profiles/last-modified");

Error类有以下方法。

// Returns the "id" member of the error
$id = $firstError->id();

// Checks if the error has the "meta" member
$hasMeta = $firstError->hasMeta();

// Retrieves the "meta" member as an array
$meta = $firstError->meta();

// Checks if the error has any links
$hasLinks = $firstError->hasLinks();

// Retrieves the "links" member as an ErrorLinks object
$links = $firstError->links();

// Returns the "status" member
$status = $firstError->status();

// Returns the "code" member
$code = $firstError->code();

// Returns the "title" member
$title = $firstError->title();

// Returns the "detail" member
$detail = $firstError->detail();

// Checks if the error has the "source" member
$hasSource = $firstError->hasSource();

// Returns the "source" member as an ErrorSource object
$source = $firstError->source();

ResourceObject类有以下方法。

// Returns the type of the resource
$type = $primaryResource->type();

// Returns the id of the resource
$id = $primaryResource->id();

// Checks if the resource has the "meta" member
$hasMeta = $primaryResource->hasMeta();

// Returns the "meta" member as an array
$meta = $primaryResource->meta();

// Checks if the resource has any links
$hasLinks = $primaryResource->hasLinks();

// Returns the "links" member as a ResourceLinks object
$links = $primaryResource->links();

// Returns the attributes of the resource as an array
$attributes = $primaryResource->attributes();

// Returns the ID and attributes of the resource as an array
$idAndAttributes = $primaryResource->idAndAttributes();

// Checks if the resource has a specific attribute
$hasFirstName = $primaryResource->hasAttribute("first_name");

// Returns an attribute of the resource or null if it is missing
$firstName = $primaryResource->attribute("first_name");

// Returns an attribute of the resource or the default value if it is missing
$lastName = $primaryResource->attribute("last_name", "");

// Returns all relationships of the resource as an array of Relationship objects
$relationships = $primaryResource->relationships();

// Checks if the resource has a specific relationship
$hasAddress = $primaryResource->hasRelationship("address");

// Returns a relationship of the resource as a Relationship object or throws an exception if it is missing
$relationship = $primaryResource->relationship("address");

Relationship对象支持以下方法。

// Checks if it is a to-one relationship
$isToOneRelationship = $relationship->isToOneRelationship();

// Checks if it is a to-many relationship
$isToManyRelationship = $relationship->isToManyRelationship();

// Returns the name of the relationship
$name = $relationship->name();

// Checks if the relationship has the "meta" member
$hasMeta = $relationship->hasMeta();

// Returns the "meta" member of the relationship as an array
$meta = $relationship->meta();

// Returns the "links" member of the relationship as a RelationshipLinks object
$links = $relationship->links();

// Returns the first resource linkage of the relationship as an array (e.g.: ["type" => "address", "id" => "123"])
// or null if there isn't any related data
$resourceLinkage = $relationship->firstResourceLink();

// Returns the resource linkage as an array of array (e.g.: [["type" => "address", "id" => "123"]])
$resourceLinkage = $relationship->resourceLinks();

// Checks if a specific resource object is included
$isIncluded = $relationship->hasIncludedResource("address", "abcd");

// Returns the resource object of a to-one relationship as a `ResourceObject` instance
// or throws an exception otherwise or when the relationship is empty
$resource = $relationship->resource();

// Returns the resource objects of a to-many relationship as an array of `ResourceObject` instances
// or throws an exception otherwise
$resources = $relationship->resources();

数据填充

使用上述方法处理具有许多相关资源的JSON:API响应并不容易。例如,如果您想检索相关资源属性值,需要以下代码

$dogResource = $response->document()->primaryResource();

$breedName = $dogResource->relationship("breed")->resource()->attribute("name");

这已经足够多了,当您想要映射具有许多关系的复杂响应文档到对象时,情况会更糟。

$dogResource = $response->document()->primaryResource();

$dog = new stdClass();
$dog->name = $dogResource->attribute("name");
$dog->age = $dogResource->attribute("age");
$dog->breed = $dogResource->relationship("breed")->resource()->attribute("name");
foreach ($dogResource->relationship("owners")->resources() as $ownerResource) {
    $owner = new stdClass();
    $owner->name = $ownerResource->attribute("name");

    $addressResource = $ownerResource->relationship("address")->resource();
    $owner->address = new stdClass();
    $owner->address->city = $addressResource->attribute("city");
    $owner->address->addressLine = $addressResource->attribute("city");

    $dog->owners[] = $owner;
}

这就是使用 hydrator 可以帮助您的情况。目前,Yang只有一个 hydrator,即ClassDocumentHydrator,如果响应成功,它将指定的文档映射到stdClass,包括所有资源属性和关系。这意味着错误、链接、元数据将不会出现在返回的对象中。然而,现在访问关系变得非常容易。

让我们使用上一个示例中的文档来展示hydrator的强大功能。

// Check if hydration is possible
if ($document->hasAnyPrimaryResources() === false) {
    return;
}

// Hydrate the document to an stdClass
$hydrator = new ClassDocumentHydrator();
$dog = $hydrator->hydrateSingleResource($response->document());

您只需要这样做就可以创建与第一个示例中相同的$dog对象!现在,您可以显示它的属性。

echo "Dog:\n";
echo "Name : " . $dog->name . "\n";
echo "Breed: " . $dog->breed->name . "\n\n";

echo "Owners:\n";
foreach ($dog->owners as $owner) {
    echo "Name   : " . $dog->owner->name . "\n";
    echo "Address: " . $dog->owner->address->city . ", " . $dog->owner->address->addressLine . "\n";
    echo "------------------\n";
}

注意:当文档没有任何主要数据或主要数据是集合时,方法ClassDocumentHydrator::hydrateSingleResource()会抛出DocumentException。否则——当主要数据是单个资源时——会返回一个包含所有属性和关系的stdObject

此外,您还可以使用ClassHydrator::hydrateCollection()方法来检索许多狗。

// Check if hydration is possible
if ($document->isSingleResourceDocument()) {
    return;
}

// Hydrate the document to an array of stdClass
$hydrator = new ClassDocumentHydrator();
$dogs = $hydrator->hydrateCollection($response->document());

注意:当主要数据是单个资源时,方法ClassHydrator::hydrateCollection()会抛出DocumentException。否则——当主要数据是资源的集合时——会返回一个包含所有属性和关系的stdObject数组。

此外,当您不关心主要数据是单个资源还是资源集合时,还有一个hydrate()方法可供您使用。

注意:当文档没有任何主要数据时,方法ClassDocumentHydrator::hydrate()返回一个空数组。当主要数据是单个资源时,它返回包含单个stdObject的数组。否则——当主要数据是资源集合时——返回一个stdObject数组。

高级用法

自定义序列化

有时您可能需要巧妙地将请求体序列化为自定义方式。例如,如果您在原始请求内部分发服务器请求(内部请求),则可以发送数组作为请求体,这要归功于这个特性——因此您不需要在客户端序列化,然后在服务器端反序列化。如果您在服务器端使用Woohoo Labs Yin和自定义反序列化器,那么这是一个简单任务。

在客户端,如果您使用Yang与请求构建器一起使用,那么您只需要将第二个构造函数参数传递给它,如下所示,以利用自定义序列化。

// Instantiate a PSR-7 request
$request = new Request();

// Instantiate your custom serializer
$mySerializer = new MyCustomSerializer();

// Instantiate the request builder with a custom serializer
$requestBuilder = new JsonApiRequestBuilder($request, $mySerializer);

您只需要确保您的自定义序列化器实现了SerializerInterface

自定义反序列化

有时你可能需要巧妙地以自定义方式反序列化服务器响应。例如,如果你在原始请求内部(内部)发送服务器请求,那么你可以利用这个特性接收响应体作为数组 - 因此你不需要在服务器端进行序列化,然后再在客户端进行反序列化。如果你在服务器端使用Woohoo Labs. Yin和自定义序列化器,那么这是一个简单的任务。

在客户端,如果你使用Yang的默认HTTP客户端,那么你只需向它们传递第二个构造函数参数,如下所示,即可利用自定义反序列化

use Http\Adapter\Guzzle7\Client;

// Instantiate the Guzzle HTTP Client
$guzzleClient = Client::createWithConfig([]);

// Instantiate your custom deserializer
$myDeserializer = new MyCustomDeserializer();

// Instantiate the syncronous JSON:API Client with a custom deserializer
$syncClient = new JsonApiClient($guzzleClient, $myDeserializer);

// Instantiate the asyncronous JSON:API Client with a custom deserializer
$asyncClient = new JsonApiAsyncClient($guzzleClient, $myDeserializer);

否则,将你的反序列化器传递给JsonApiResponse作为其第二个参数,如下所示

// Instantiate a JSON:API response from a PSR-7 response with a custom deserializer
$response = new JsonApiResponse($psr7Response, new MyCustomDeserializer());

你只需确保你的自定义反序列化器实现了DeserializerInterface

示例

请查看示例目录以获取一个非常基本的示例。

版本控制

此库遵循SemVer v2.0.0

变更日志

有关最近更改的更多信息,请参阅变更日志

测试

Woohoo Labs. Yang有一个PHPUnit测试套件。要运行测试,请在项目文件夹中运行以下命令

$ phpunit

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

贡献

有关详细信息,请参阅贡献指南

支持

有关详细信息,请参阅支持

致谢

许可证

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