woohoolabs / yang
Woohoo Labs. Yang
Requires
- php: ^7.4.0||^8.0.0
- php-http/httplug: ^2.0.0
- psr/http-client: ^1.0.0
- psr/http-message-implementation: ^1.0.0
Requires (Dev)
- guzzlehttp/psr7: ^2.6.0
- php-http/guzzle7-adapter: ^1.0.0
- phpstan/phpstan: ^1.10.12
- phpstan/phpstan-phpunit: ^1.3.0
- phpstan/phpstan-strict-rules: ^1.5.0
- phpunit/phpunit: ^9.6.0
- squizlabs/php_codesniffer: ^3.7.0
- woohoolabs/coding-standard: ^2.4.0
- woohoolabs/releaser: ^1.1.0
Suggests
- php-http/guzzle7-adapter: Allows to use Guzzle 7 as the HTTP client implementation
This package is auto-updated.
Last update: 2024-09-06 11:49:23 UTC
README
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-18和HTTPlug提供易于使用的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-18和HTTPlug,因此您可以选择如何发送请求。如果您已安装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 up
或make test
来执行测试。
贡献
有关详细信息,请参阅贡献指南。
支持
有关详细信息,请参阅支持。
致谢
许可证
MIT许可证(MIT)。有关更多信息,请参阅许可文件。