swisnl/json-api-client

一个用于将远程 JSON:API 资源映射到 Eloquent 模型和集合的 PHP 扩展包。

2.4.0 2024-05-15 07:16 UTC

README

PHP from Packagist Latest Version on Packagist Software License Buy us a tree Build Status Scrutinizer Coverage Scrutinizer Code Quality Made by SWIS

一个用于将远程 JSON:API 资源映射到 Eloquent 模型和集合的 PHP 扩展包。

💡 在开始之前,请注意,此库仅适用于 JSON:API 资源,并需要了解一些基本规范。如果您不熟悉 {json:api},请阅读 Björn Brala 的优秀博客 以获取快速介绍。

安装

ℹ️ 使用 Laravel?请查看 swisnl/json-api-client-laravel 以实现简单的 Laravel 集成。

composer require swisnl/json-api-client

N.B. 在安装此扩展包之前,请确保您已安装了 PSR-18 HTTP 客户端和 PSR-17 HTTP 工厂,或者同时安装它们,例如 composer require swisnl/json-api-client guzzlehttp/guzzle:^7.3

HTTP 客户端

我们通过 PSR-18 HTTP 客户端PSR-17 HTTP 工厂 与任何 HTTP 消息客户端解耦。这需要额外安装提供 psr/http-client-implementationpsr/http-factory-implementation 的包。例如,要使用 Guzzle 7,只需要求 guzzlehttp/guzzle

composer require guzzlehttp/guzzle:^7.3

如果想要使用自己的 HTTP 客户端或使用特定的配置选项,请参阅 HTTP 客户端

入门

您可以直接创建一个 DocumentClient 实例并在您的类中使用它。或者,您可以创建一个 仓库

use Swis\JsonApi\Client\DocumentClient;

$client = DocumentClient::create();
$document = $client->get('https://cms.contentacms.io/api/recipes');

/** @var \Swis\JsonApi\Client\Collection&\Swis\JsonApi\Client\Item[] $collection */
$collection = $document->getData();

foreach ($collection as $item) {
  // Do stuff with the items
}

条目

默认情况下,所有条目都是 \Swis\JsonApi\Client\Item 的一个实例。该 Item 提供了一个类似 Laravel Eloquent 的基础类。

您可以通过扩展 \Swis\JsonApi\Client\Item 或自己实现 \Swis\JsonApi\Client\Interfaces\ItemInterface 来定义自己的模型。如果您想定义隐藏属性、类型转换或获取/设置修改器,这可能很有用。如果您使用自定义模型,您必须使用 TypeMapper 进行注册。

关系

此扩展包实现了类似 Laravel Eloquent 的关系。这些关系提供了一个流畅的接口来检索相关条目。目前有四种关系可用

  • HasOneRelation
  • HasManyRelation
  • MorphToRelation
  • MorphToManyRelation

请参阅以下示例,了解如何定义关系

use Swis\JsonApi\Client\Item;

class AuthorItem extends Item
{
    protected $type = 'author';

    public function blogs()
    {
        return $this->hasMany(BlogItem::class);
    }
}

class BlogItem extends Item
{
    protected $type = 'blog';

    public function author()
    {
        return $this->hasOne(AuthorItem::class);
    }
}

命名支持

应使用 camelCase 方法定义关系。然后,您可以通过 camelCase 或 snake_case 的魔法属性访问相关项目,或使用定义关系时使用的显式名称。

集合

此扩展包使用 Laravel Collections 作为项目数组的包装器。

链接

所有可以具有链接的对象(即文档、错误、条目和关系)都使用Concerns/HasLinks,因此具有一个返回Links实例的getLinks方法。这是一个简单的类似数组的对象,其键值对是Link实例或null

示例

给定以下JSON

{
	"links": {
		"self": "http://example.com/articles"
	},
	"data": [{
		"type": "articles",
		"id": "1",
		"attributes": {
			"title": "JSON:API paints my bikeshed!"
		},
		"relationships": {
			"author": {
				"data": {
					"type": "people",
					"id": "9"
				},
				"links": {
					"self": "http://example.com/articles/1/author"
				}
			}
		},
		"links": {
			"self": "http://example.com/articles/1"
		}
	}]
}

你可以这样获取链接

/** @var $document \Swis\JsonApi\Client\Document */

// Document links
$links = $document->getLinks();
echo $links->self->getHref(); // http://example.com/articles

// Item links
$links = $document->getData()->getLinks();
echo $links->self->getHref(); // http://example.com/articles/1

// Relationship links
$links = $document->getData()->author()->getLinks();
echo $links->self->getHref(); // http://example.com/articles/1/author

元数据

所有可以具有元信息的对象(即文档、错误、条目、jsonapi、链接和关系)都使用Concerns/HasMeta,因此具有一个返回Meta实例的getMeta方法。这是一个简单的类似数组的对象,包含键值对。

示例

给定以下JSON

{
	"links": {
		"self": {
			"href": "http://example.com/articles/1",
			"meta": {
				"foo": "bar"
			}
		}
	},
	"data": {
		"type": "articles",
		"id": "1",
		"attributes": {
			"title": "JSON:API paints my bikeshed!"
		},
		"relationships": {
			"author": {
				"data": {
					"type": "people",
					"id": "9"
				},
				"meta": {
					"written_at": "2019-07-16T13:47:26"
				}
			}
		},
		"meta": {
			"copyright": "Copyright 2015 Example Corp."
		}
	},
	"meta": {
		"request_id": "a77ab2b4-7132-4782-8b5e-d94ebaff6e13"
	}
}

你可以这样获取元数据

/** @var $document \Swis\JsonApi\Client\Document */

// Document meta
$meta = $document->getMeta();
echo $meta->request_id; // a77ab2b4-7132-4782-8b5e-d94ebaff6e13

// Link meta
$meta = $document->getLinks()->self->getMeta();
echo $meta->foo; // bar

// Item meta
$meta = $document->getData()->getMeta();
echo $meta->copyright; // Copyright 2015 Example Corp.

// Relationship meta
$meta = $document->getData()->author()->getMeta();
echo $meta->written_at; // 2019-07-16T13:47:26

类型映射器

所有自定义模型都必须在TypeMapper中注册。正如其名所示,TypeMapper将JSON:API类型映射到自定义条目

仓库

为了方便,此包包含一个基本的仓库,其中包含一些用于处理资源的方法。您可以根据\Swis\JsonApi\Client\Repository创建每个端点对应的仓库。然后,此仓库将使用标准的CRUD端点执行所有操作。

class BlogRepository extends \Swis\JsonApi\Client\Repository
{
    protected $endpoint = 'blogs';
}

上述仓库将包含所有CRUD操作的函数。如果您正在与只读API一起工作且不想执行所有操作,您可以通过扩展\Swis\JsonApi\Client\BaseRepository并包含所需的操作/特性来构建自己的仓库。

use Swis\JsonApi\Client\Actions\FetchMany;
use Swis\JsonApi\Client\Actions\FetchOne;

class BlogRepository extends \Swis\JsonApi\Client\BaseRepository
{
    use FetchMany;
    use FetchOne;
    
    protected $endpoint = 'blogs';
}

如果此仓库(模式)不符合您的需求,您可以使用此包提供的客户端创建自己的实现。

请求参数

仓库提供的所有方法都接受额外的参数,这些参数将附加到URL上。这可以用于添加包含和/或分页参数

$repository = new BlogRepository();
$repository->all(['include' => 'author', 'page' => ['limit' => 15, 'offset' => 0]]);

条目填充器

ItemHydrator可用于使用具有属性的关联数组填充/填充一个条目及其关系。如果您想使用请求的POST数据填充一个条目,这将很有用。

$typeMapper = new TypeMapper();
$itemHydrator = new ItemHydrator($typeMapper);
$blogRepository = new BlogRepository(DocumentClient::create($typeMapper), new DocumentFactory());

$item = $itemHydrator->hydrate(
    $typeMapper->getMapping('blog'),
    request()->all(['title', 'author', 'date', 'content', 'tags']),
    request()->id
);
$blogRepository->save($item);

关系

ItemHydrator还可以填充(嵌套)关系。必须明确在$availableRelations数组中列出关系,以便进行填充。如果我们考虑上面的示例,我们可以使用以下属性数组来填充一个新的博客条目

$attributes = [
    'title'   => 'Introduction to JSON:API',
    'author'  => [
        'id'       => 'f1a775ef-9407-40ba-93ff-7bd737888dc6',
        'name'     => 'Björn Brala',
        'homepage' => 'https://github.com/bbrala',
    ],
    'co-author' => null,
    'date'    => '2018-12-02 15:26:32',
    'content' => 'JSON:API was originally drafted in May 2013 by Yehuda Katz...',
    'media' => [],
    'tags'    => [
        1,
        15,
        56,
    ],
];
$itemDocument = $itemHydrator->hydrate($typeMapper->getMapping('blog'), $attributes);

echo json_encode($itemDocument, JSON_PRETTY_PRINT);

{
    "data": {
        "type": "blog",
        "attributes": {
            "title": "Introduction to JSON:API",
            "date": "2018-12-02 15:26:32",
            "content": "JSON:API was originally drafted in May 2013 by Yehuda Katz..."
        },
        "relationships": {
            "author": {
                "data": {
                    "type": "author",
                    "id": "f1a775ef-9407-40ba-93ff-7bd737888dc6"
                }
            },
            "co-author": {
                "data": null
            },
            "media": {
                "data": []
            },
            "tags": {
                "data": [{
                    "type": "tag",
                    "id": "1"
                }, {
                    "type": "tag",
                    "id": "15"
                }, {
                    "type": "tag",
                    "id": "56"
                }]
            }
        }
    },
    "included": [{
        "type": "author",
        "id": "f1a775ef-9407-40ba-93ff-7bd737888dc6",
        "attributes": {
            "name": "Björn Brala",
            "homepage": "https://github.com/bbrala"
        }
    }]
}

如示例所示,关系可以通过id或通过包含id和更多属性的关联数组进行填充。如果使用关联数组填充条目,除非在关系上调用setOmitIncluded(true),否则它将包含在生成的json中。可以通过为单数关系传递null或为复数关系传递空数组来取消设置关系。

注意:形态关系需要数据中存在'type'属性,以便知道应创建哪种类型的条目。

处理错误

请求可能因多个原因而失败,并且如何处理这取决于发生了什么。如果DocumentClient遇到错误,基本有三个选项。

非2xx请求且无正文

如果响应没有成功的状态代码(2xx)并且没有正文,则DocumentClient(因此也是Repository)将返回一个InvalidResponseDocument实例。

非2xx请求且JSON:API正文无效

如果响应没有成功的状态代码(2xx)并且有正文,它将被解析为JSON:API文档。如果响应不能解析为这种文档,将抛出ValidationException

非2xx请求且JSON:API正文有效

如果一个响应没有成功状态码(2xx)但有正文,它将被解析为JSON:API文档。在这种情况下,DocumentClient(因此也包括Repository)将返回一个Document实例。该文档包含响应中的错误,假设服务器响应了错误。

检查错误

根据上述规则,您可以这样检查错误:

$document = $repository->all();

if ($document instanceof InvalidResponseDocument || $document->hasErrors()) {
    // do something with errors
}

客户端

本包提供两个客户端:DocumentClientClient

DocumentClient

这是您通常会使用的客户端,例如,仓库在内部使用此客户端。根据JSON:API规范,所有请求和响应都是文档。因此,此客户端在发送数据时始终期望一个\Swis\JsonApi\Client\Interfaces\DocumentInterface作为输入,并始终返回此相同接口。这可以是一个空的Document,一个项目的ItemDocument,一个集合的CollectionDocument,或者在服务器响应非2xx响应时返回一个InvalidResponseDocument

DocumentClient在内部遵循以下步骤

  1. 使用您的HTTP客户端发送请求;
  2. 使用ResponseParser解析和验证响应;
  3. 创建正确的文档实例;
  4. 通过使用与TypeMapper注册的项模型或\Swis\JsonApi\Client\Item作为后备,为每个项进行填充;
  5. 为所有关系进行填充;
  6. 向文档添加元数据,例如错误链接元数据

Client

这是一个更底层的客户端,可以用于例如发送二进制数据(如图像)等。它可以接收请求工厂接受的任何输入数据,并返回原始的\Psr\Http\Message\ResponseInterface。它不会解析或验证响应或填充项!

DocumentFactory

DocumentClient在创建或更新资源时需要ItemDocumentInterface实例。可以使用DocumentFactory轻松地创建此类文档,只需提供DataInterface实例即可。这可以是一个ItemInterface,通常由ItemHydrator创建,或者是一个Collection

HTTP客户端

默认情况下,Client使用php-http/discovery来查找可用的HTTP客户端、请求工厂和流工厂,因此您无需自己设置这些。您也可以指定自己的HTTP客户端、请求工厂或流工厂。这是向HTTP客户端添加额外选项或为测试注册模拟HTTP客户端的绝佳方式

if (app()->environment('testing')) {
    $httpClient = new \Swis\Http\Fixture\Client(
        new \Swis\Http\Fixture\ResponseBuilder('/path/to/fixtures')
    );
} else {
    $httpClient = new \GuzzleHttp\Client(
        [
            'http_errors' => false,
            'timeout' => 2,
        ]
    );
}

$typeMapper = new TypeMapper();
$client = DocumentClient::create($typeMapper, $httpClient);
$document = $client->get('https://cms.contentacms.io/api/recipes');

注意。此示例在测试环境中使用我们的swisnl/php-http-fixture-client。此包允许您轻松地使用静态固定值模拟请求。绝对值得一试!

高级使用

如果您不喜欢使用提供的仓库或客户端,您还可以使用Parsers\ResponseParserParser\DocumentParser分别解析'raw' \Psr\Http\Message\ResponseInterface或简单的json字符串。

变更日志

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

测试

composer test

贡献

请参阅CONTRIBUTINGCODE_OF_CONDUCT以获取详细信息。

安全

如果您发现任何安全相关的问题,请通过电子邮件security@swis.nl联系,而不是使用问题跟踪器。

许可证

MIT许可证(MIT)。请参阅许可证文件获取更多信息。

本软件包是Treeware。如果您在生产环境中使用它,我们希望您购买一棵树以感谢我们的工作。通过为Treeware森林做出贡献,您将为当地家庭创造就业机会并恢复野生动物栖息地。

SWIS ❤️ 开源

SWIS是一家来自荷兰莱顿的网站代理机构。我们热爱与开源软件合作。