Eloquent API 调用
发布日期 作者 Steve McDougall
我一直在谈论 API 集成,每次我都会发现比上一次的改进,又一次冲进战场。
我最近在推特上被标记到一篇名为 SDKs, The Laravel Way 的文章。这很突出,因为它在某种程度上反映了我进行 API 集成的方式,并激励我在上面添砖加瓦。
记录一下,我提到的文章非常棒,它采用的方法并没有错。我觉得我可以根据自己在 API 开发和集成中积累的经验来进一步扩展它。我不会直接复制主题,因为它与我不熟悉的 API 相关。但是,我会以我熟悉的 API(GitHub API)中的概念为例,并深入探讨一些细节。
在构建新的 API 集成时,我首先整理配置和环境变量。在我看来,这些应该放在 services
配置文件中,因为它们是我们正在配置的第三方服务。当然,你不需要这样做,而且它们在哪里存放并不重要,只要你能记住它们的位置!
// config/services.php return [ // other services 'github' => [ 'url' => env('GITHUB_URL'), 'token' => env('GITHUB_TOKEN'), 'timeout' => env('GITHUB_TIMEOUT', 15), ],];
我要确保做到的主要事情是存储 URL、API 令牌和超时时间。我存储 URL,因为我讨厌在类或应用程序代码中出现浮动字符串。感觉很糟糕,当然它并不错误,但我们都知道我观点很强硬……
接下来,我创建了一个契约/接口——根据有多少集成将告诉我我可能需要哪种接口。我喜欢使用接口,因为它强制在代码中执行契约,并迫使我在更改 API 之前思考。我保护自己免受重大变更!但是,我的契约非常简单。我通常有一个方法和一个文档块,用于我打算通过构造函数添加的属性。
/** * @property-read PendingRequest $request */interface ClientContract{ public function send();}
我将发送请求的责任留给客户端。此方法稍后会被重构,但我稍后会详细介绍。此接口可以放在你最习惯的地方——我通常把它放在 App\Services\Contracts
下,但请随意发挥想象力。
一旦我有了粗略的契约,就开始构建实现本身。我喜欢为每个我构建的集成创建一个新的命名空间。它可以将事物分组并保持逻辑性。
final class Client implements ClientContract{ public function __construct( private readonly PendingRequest $request, ) {}}
我已经开始将配置好的 PendingRequest
传递到 API 客户端,因为它可以保持代码简洁,避免手动设置。我喜欢这种方法,想知道我为什么以前没有这样做!
你会注意到我仍然需要遵循契约,因为我需要先执行一个步骤。我最喜欢的 PHP 8.1 功能之一——**枚举**。
我为应用程序创建了一个方法枚举,我应该做一个包,因为它可以保持事物流畅——同样,在我的应用程序中没有浮动字符串!
enum Method: string{ case GET = 'GET'; case POST = 'POST'; case PUT = 'PUT'; case PATCH = 'PATCH'; case DELETE = 'DELETE';}
我一开始保持简单,并在需要时扩展——也只在需要时扩展。我涵盖了将使用的主要 HTTP 动词,并在需要时添加更多。
我可以重构我的契约以包含我想让它如何工作。
/** * @property-read PendingRequest $request */interface ClientContract{ public function send(Method $method, string $url, array $options = []): Response;}
我的客户端的发送方法应该知道使用的方法、发送到的 URL 和任何需要的选项。这几乎与 PendingRequest
的发送方法相同——除了使用枚举表示方法之外,这是有意的。
但是,我没有将此添加到我的客户端,因为我可能有多个客户端想要发送请求。因此我创建了一个关注点/特征,可以将其添加到每个客户端,使它能够发送请求。
/** * @mixin ClientContract */trait SendsRequests{ public function send(Method $method, string $url, array $options = []): Response { return $this->request->throw()->send( method: $method->value, url: $url, options: $options, ); }}
这使我发送 API 请求的方式标准化,并强制它们自动抛出异常。我现在可以将此行为添加到我的客户端本身,使它更简洁、更精简。
final class Client implements ClientContract{ use SendsRequests; public function __construct( private readonly PendingRequest $request, ) {}}
从这里开始,我开始确定资源范围以及我希望资源负责什么。此时,一切都是关于对象设计的。对我来说,资源就是终结点,通过 HTTP 传输层提供的一个外部资源。它不需要比这更多。
像往常一样,我创建了一个契约/接口,我希望所有资源都遵循它,这意味着我拥有可预测的代码。
/** * @property-read ClientContract $client */interface ResourceContract{ public function client(): ClientContract;}
我们希望我们的资源能够通过 getter 方法访问客户端本身。我们也添加客户端作为文档块属性。
现在我们可以创建我们的第一个资源。我们将关注 Issues,因为它是一个非常令人兴奋的终结点。让我们从创建类并对其进行扩展开始。
final class IssuesResource implements ResourceContract{ use CanAccessClient;}
我在此处创建了一个新的特征/关注点,称为 CanAccessClient
,因为我们所有的资源,无论是什么 API,都想要访问它的父客户端。我还将构造函数移到了这个特征/关注点中——我偶然发现它可以工作,并且很喜欢它。我仍在决定是否始终这样做或保持原样,但它可以使我的资源更简洁、更专注——所以我现在会保持原样。不过,我很想听听你对此的看法!
/** * @mixin ResourceContract */trait CanAccessClient{ public function __construct( private readonly ClientContract $client, ) {} public function client(): ClientContract { return $this->client; }}
现在我们有了资源,我们可以让我们的客户端了解它——并开始展望集成中最令人兴奋的部分:请求。
final class Client implements ClientContract{ use SendsRequests; public function __construct( private readonly PendingRequest $request, ) {} public function issues(): IssuesResource { return new IssuesResource( client: $this, ); }}
这使我们能够拥有一个简洁干净的 API $client->issues()->
,因此我们不会依赖于魔术方法或代理任何东西——它对于我们的 IDE 来说是干净且可发现的。
我们想要能够发送的第一个请求是列出已认证用户的全部 Issues。此 API 终结点是 https://api.github.com/issues
,非常简单。现在让我们看看我们的请求以及我们想要如何发送它们。没错,你猜对了,我们再次需要一个契约/接口。
/** * @property-read ResourceContract $resource */interface RequestContract{ public function resource(): ResourceContract;}
我们的请求将实现一个关注点/特征,使它能够调用资源并将所需的请求传回客户端。
/** * @mixin RequestContract */trait HasResource{ public function __construct( private ResourceContract $resource, ) {} public function resource(): ResourceContract { return $this->resource; }}
最后,我们可以开始考虑我们想要发送的请求!要达到这个阶段,需要很多样板代码。但是,从长远来看,它将是值得的。我们可以微调并随时更改此链中的任何部分,而不会引入重大变更。
final class ListIssuesRequest implements RequestContract{ use HasResource; public function __invoke(): Response { return $this->resource()->client()->send( method: Method::GET, url: 'issues', ); }}
此时,我们可以开始考虑是否需要对请求进行转换。我们只将其设置为直接返回响应,但如果需要,我们可以进一步重构它。我们使我们的请求可调用,以便我们可以直接调用它。我们的 API 现在如下所示
$client->issues()->list();
我们正在使用 Illuminate 响应来访问此处的數據,因此它具有我们可能想要的所有便利方法。但是,这并非总是理想的。有时我们想在应用程序中使用更易于使用的对象。为此,我们需要考虑转换响应。
我不会在此处过多介绍如何转换响应以及你可以做什么,因为我认为这将是一个很棒的教程。如果你发现此教程有用,或者对改进此过程有任何建议,请告诉我们。