在 Laravel 中使用 Saloon 进行 API 集成
发布于 作者: Steve McDougall
我们都遇到过这种情况,我们想要在 Laravel 中与第三方 API 集成,然后我们会问自己“我应该怎么做?”。说到 API 集成,我并不陌生,但每次我都会想哪种方式最好。 Sam Carré 在 2022 年初构建了一个名为 包 Saloon,它可以使我们的 API 集成变得非常棒。然而,这篇文章将有所不同,我们将逐步介绍如何从头开始使用它来构建一个集成。
像所有伟大的事物一样,它从 laravel new
开始,并由此发展,所以让我们开始吧。现在,说到安装 Laravel,你可以使用 Laravel 安装程序或 composer - 这部分由你决定。如果可以,我建议使用安装程序,因为它提供了除创建项目之外更多功能的简单选项。创建一个新项目,并在你选择的代码编辑器中打开它。一旦我们到达那里,就可以开始了。
我们将要构建什么?我很高兴你问!我们将要构建一个与 GitHub API 集成,以获取存储库中可用工作流程的列表。现在,如果你像我一样,经常在命令行工作,这将非常有用。你在开发一个应用程序,将更改推送到分支或创建 PR - 它会经过一个可能正在运行许多其他内容的工作流程。有时,了解此工作流程的状态会对你的下一步操作产生重大影响。该功能是否已完成?我们的工作流程运行是否遇到问题?我们的测试或静态分析是否通过?所有这些东西你通常都需要等待并检查 GitHub 上的存储库以查看状态。此集成将允许你运行一个 artisan 命令,获取存储库中可用工作流程的列表,并允许你触发新的工作流程运行。
所以到目前为止,composer 应该已经完成了它的工作并安装了完美的起点,一个 Laravel 应用程序。接下来,我们需要安装 Saloon - 但我们要确保安装 Laravel 版本,所以在终端中运行以下命令
composer require sammyjo20/saloon-laravel
就是这样,我们离更轻松的集成又近了一步。如果你在此阶段遇到任何问题,请确保检查你正在使用的 Laravel 和 PHP 版本,因为 Saloon 至少需要 Laravel 8 和 PHP 8!
所以,现在我们已经安装了 Saloon,我们需要创建一个新的类。在 Saloon 的术语中,这些是“连接器”,连接器所做的一切就是以对象为中心的方式说 - 此 API 通过此类连接。有一个方便的 artisan 命令允许你创建这些连接器,所以运行以下 artisan 命令来创建一个 GitHub 连接器
php artisan saloon:connector GitHub GitHubConnector
此命令分为两部分,第一个参数是你要创建的集成,第二个参数是要创建的连接器的名称。这意味着你可以在一个集成中创建多个连接器 - 这为你提供了许多控制权,以便根据需要以许多不同的方式进行连接。
这将为你创建一个新的类,位于 app/Http/Integrations/GitHub/GitHubConnector.php
下,让我们看一看,了解一下发生了什么。
我们首先看到的是,我们的连接器扩展了 SaloonConnector
,这将允许我们在没有太多样板代码的情况下使我们的连接器工作。然后我们继承了一个名为 AcceptsJson
的特征。现在,如果我们查看 Saloon 文档,我们知道这是一个插件。这基本上是在我们的请求中添加一个标题,告诉第三方 API 我们想接受 JSON 响应。我们接下来看到的是,我们有一个方法用于定义连接器的基本 URL - 所以让我们添加我们的
public function defineBaseUrl(): string{ return 'https://api.github.com';}
简洁明了,我们甚至可以更进一步,这样我们就可以处理应用程序中更少的松散字符串 - 因此让我们看看如何做到这一点。在你的 config/services.php
文件中添加一个新的服务记录
'github' => [ 'url' => env('GITHUB_API_URL', 'https://api.github.com'),]
这将允许我们覆盖不同的环境 - 为我们提供更好的、更可测试的解决方案。在本地,我们甚至可以使用 GitHub API 的 OpenAPI 规范模拟 GitHub API,并针对它进行测试,以确保它能正常工作。然而,本教程是关于 Saloon 的,所以我离题了......现在让我们重构我们的基本 URL 方法以使用配置
public function defineBaseUrl(): string{ return (string) config('services.github.url');}
如你所见,我们现在从我们的配置中获取新添加的记录 - 并将其转换为字符串以确保类型安全 - config()
返回混合结果,因此如果可以,我们希望对此严格一些。
接下来是默认标头和默认配置,现在我不会担心默认标头,因为我们将在稍后单独处理身份验证。但配置是我们定义集成 Guzzle 选项的地方,因为 Saloon 在幕后使用 Guzzle。现在,让我们设置超时并继续,但请随意根据你的需要花时间配置它
public function defaultConfig(): array{ return [ 'timeout' => 30, ];}
现在我们已经按照需要配置了连接器,我们现在可以稍后回来,如果发现需要添加任何东西。下一步是开始考虑我们要发送的请求。如果我们查看 GitHub Actions API 的 API 文档,我们会看到很多选项,我们将从列出特定存储库的工作流程开始:/repos/{owner}/{repo}/actions/workflows
。运行以下 artisan 命令创建新的请求
php artisan saloon:request GitHub ListRepositoryWorkflowsRequest
同样,第一个参数是集成,第二个参数是我们想要创建的请求的名称。我们需要确保为我们创建的请求命名集成,以便它位于正确的位置,然后我们需要给它一个名称。我将我的请求命名为 ListRepositoryWorkflowsRequest
,因为我喜欢描述性的命名方法 - 然而,请随意根据你喜欢的命名方式进行调整,因为这里没有真正错误的方法。这将为我们创建一个新的文件供我们查看:app/Http/Integrations/GitHub/Requests/ListRepositoryWorkflowsRequest.php
- 让我们现在看一下。
同样,我们在这里扩展了库类,这次是 SaloonRequest
,这是可以预料到的。然后我们有一个连接器属性和一个方法。如果需要,我们可以更改方法 - 但当前我们需要的默认方法是 GET
。然后我们有一个方法用于定义端点。重构你的请求类,使其看起来像下面的示例
class ListRepositoryWorkflowsRequest extends SaloonRequest{ protected ?string $connector = GitHubConnector::class; protected ?string $method = Saloon::GET; public function __construct( public string $owner, public string $repo, ) {} public function defineEndpoint(): string { return "/repos/{$this->owner}/{$this->repo}/actions/workflows"; }}
我们所做的是添加了一个构造函数,它接受 repo 和 owner 作为参数,我们可以在定义端点方法中使用它们。我们还将连接器设置为我们之前创建的 GitHubConnector
。因此我们有一个我们知道可以发送的请求,我们可以从集成中稍微退一步,考虑 Console Command。
如果你之前从未在 Laravel 中创建过控制台命令,请确保查看 文档,它非常棒。运行以下 artisan 命令来创建此集成的第一个命令
php artisan make:command GitHub/ListRepositoryWorkflows
这将创建以下文件:app/Console/Commands/GitHub/ListRespositoryWorkflows.php
。现在我们可以开始使用我们的命令来发送请求并获取我们关心的数据。我做所有控制台命令的第一件事,就是考虑命令签名。我想要如何调用它?它需要解释它在做什么,但也需要容易记忆。我将我的命令命名为github:workflows
,因为对我来说,它能很好地解释其功能。我们还可以为控制台命令添加描述,以便在浏览可用命令时,更好地解释其用途:“通过仓库名称从 GitHub 获取工作流列表”。
最后,我们到了命令的 handle 方法,这是我们真正需要做些什么的地方。在我们的例子中,我们将发送请求,获取一些数据并以某种方式显示这些数据。但是,在我们这样做之前,有一件事我们还没有做。那就是身份验证。在每个 API 集成中,身份验证都是关键方面之一 - 我们需要 API 不仅知道我们是谁,还需要知道我们实际上是否有权发出此请求。如果您访问 GitHub 设置 并点击开发者设置和个人访问令牌,您将能够在此处生成您自己的令牌。我建议使用这种方法,而不是为此使用完整的 OAuth 应用程序。我们不需要 OAuth,我们只需要用户能够访问他们需要的内容。
获得访问令牌后,我们需要将其添加到我们的 .env
文件中,并确保我们能够通过配置获取它。
GITHUB_API_TOKEN=ghp_loads-of-letters-and-numbers-here
我们现在可以在 config/services.php
中的 github 下扩展我们的服务,以添加此令牌
'github' => [ 'url' => env('GITHUB_API_URL', 'https://api.github.com'), 'token' => env('GITHUB_API_TOKEN'),]
现在我们有了一种加载此令牌的好方法,我们可以回到我们的控制台命令!我们需要修改我们的签名,以允许我们接受所有者和仓库作为参数
class ListRepositoryWorkflows extends Command{ protected $signature = 'github:workflows {owner : The owner or organisation.} {repo : The repository we are looking at.} '; protected $description = 'Fetch a list of workflows from GitHub by the repository name.'; public function handle(): int { return 0; }}
现在我们可以将重点转向 handle 方法
public function handle(): int{ $request = new ListRepositoryWorkflowsRequest( owner: $this->argument('owner'), repo: $this->argument('repo'), ); return self::SUCCESS;}
在这里,我们开始通过将参数直接传递给请求本身来构建请求,但是我们可能想要做的是创建一些本地变量来提供一些控制台反馈
public function handle(): int{ $owner = (string) $this->argument('owner'); $repo = (string) $this->argument('repo'); $request = new ListRepositoryWorkflowsRequest( owner: $owner, repo: $repo, ); $this->info( string: "Fetching workflows for {$owner}/{$repo}", ); return self::SUCCESS;}
所以我们对用户有一些反馈,这在控制台命令中非常重要。现在我们需要添加身份验证令牌并实际发送请求
public function handle(): int{ $owner = (string) $this->argument('owner'); $repo = (string) $this->argument('repo'); $request = new ListRepositoryWorkflowsRequest( owner: $owner, repo: $repo, ); $request->withTokenAuth( token: (string) config('services.github.token'), ); $this->info( string: "Fetching workflows for {$owner}/{$repo}", ); $response = $request->send(); return self::SUCCESS;}
如果您修改上述代码,并在 $response->json()
上执行 dd()
,暂时这样。然后运行命令
php artisan github:workflows laravel laravel
这将获取 laravel/laravel
仓库的工作流列表。我们的命令将允许您使用任何公共仓库,如果您想更具体地操作,可以构建一个您想要检查的仓库选项列表,而不是接受参数 - 但这部分取决于您。在本教程中,我将重点介绍更广泛的开放用例。
现在我们从 GitHub API 获取的响应很好,也很有信息量,但它需要进行转换才能显示,如果我们单独查看它,就没有任何上下文。相反,我们将向我们的请求添加另一个插件,它将允许我们将响应转换为 DTO(领域传输对象),这是一种处理此问题的绝佳方法。它将允许我们摆脱我们习惯从 API 获取的灵活数组,并获得更具上下文感知的对象。让我们为工作流创建一个 DTO,创建一个新文件:app/Http/Integrations/GitHub/DataObjects/Workflow.php
,并在其中添加以下代码
class Workflow{ public function __construct( public int $id, public string $name, public string $state, ) {} public static function fromSaloon(array $workflow): static { return new static( id: intval(data_get($workflow, 'id')), name: strval(data_get($workflow, 'name')), state: strval(data_get($workflow, 'state')), ); } public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'state' => $this->state, ]; }}
我们有一个构造函数,其中包含我们想要显示的工作流的重要部分,一个 fromSaloon
方法,它将从 saloon 响应中转换数组到新的 DTO,以及一个 to array 方法,用于在需要时将 DTO 显示回数组。在我们的 ListRepositoryWorkflowsRequest
中,我们需要继承一个新的 trait 并添加一个新方法
class ListRepositoryWorkflowsRequest extends SaloonRequest{ use CastsToDto; protected ?string $connector = GitHubConnector::class; protected ?string $method = Saloon::GET; public function __construct( public string $owner, public string $repo, ) {} public function defineEndpoint(): string { return "/repos/{$this->owner}/{$this->repo}/actions/workflows"; } protected function castToDto(SaloonResponse $response): Collection { return (new Collection( items: $response->json('workflows'), ))->map(function ($workflow): Workflow { return Workflow::fromSaloon( workflow: $workflow, ); }); }}
我们继承了 CastsToDto
trait,它允许此请求在响应上调用 dto
方法,然后我们添加一个 castToDto
方法,我们可以在其中控制转换方式。我们希望这返回一个新的集合,因为有多个工作流,使用响应主体中的 workflows 部分。然后,我们在集合中的每个项目上进行映射 - 并将其转换为 DTO。现在我们可以通过这种方式来做,或者我们可以通过这种方式来做,我们在其中使用 DTO 构建我们的集合
protected function castToDto(SaloonResponse $response): Collection{ return new Collection( items: $response->collect('workflows')->map(fn ($workflow) => Workflow::fromSaloon( workflow: $workflow ), ) );}
您可以选择最适合您的方法。我个人更喜欢第一种方法,因为我喜欢逐步查看逻辑,但两种方法都没有错 - 选择权在您手中。现在回到命令,我们需要考虑如何显示这些信息
public function handle(): int{ $owner = (string) $this->argument('owner'); $repo = (string) $this->argument('repo'); $request = new ListRepositoryWorkflowsRequest( owner: $owner, repo: $repo, ); $request->withTokenAuth( token: (string) config('services.github.token'), ); $this->info( string: "Fetching workflows for {$owner}/{$repo}", ); $response = $request->send(); if ($response->failed()) { throw $response->toException(); } $this->table( headers: ['ID', 'Name', 'State'], rows: $response ->dto() ->map(fn (Workflow $workflow) => $workflow->toArray() )->toArray(), ); return self::SUCCESS;}
所以我们创建了一个表格,其中包含标题,然后对于行,我们想要响应 DTO,我们将遍历返回的集合,将每个 DTO 转换回数组以显示。这可能看起来违反直觉,将响应数组转换为 DTO,然后再转换回数组,但这样做将强制执行类型,以便在预期时始终存在 ID、名称和状态,并且不会产生任何奇怪的结果。它允许一致性,而普通的响应数组可能没有一致性,如果我们想,我们可以将它转换为 Value Object,在其中附加行为。如果我们现在运行我们的命令,我们应该会看到一个漂亮的表格输出,比几行字符串更容易阅读
php artisan github:workflows laravel laravel
Fetching workflows for laravel/laravel+----------+------------------+--------+| ID | Name | State |+----------+------------------+--------+| 12345678 | pull requests | active || 87654321 | Tests | active || 18273645 | update changelog | active |+----------+------------------+--------+
最后,只列出这些工作流很好 - 但是为了科学,让我们更进一步。假设您在针对其中一个仓库运行此命令,并且您想手动运行更新变更日志?或者也许您希望它在您的实时生产服务器或您可能想到的任何事件上使用 cron 触发?我们可以将变更日志设置为每天午夜运行一次,以便我们在变更日志中获得每日摘要或我们可能想要的任何内容。让我们创建一个新的控制台命令来创建一个新的工作流调度事件
php artisan saloon:request GitHub CreateWorkflowDispatchEventRequest
在这个新文件 app/Http/Integrations/GitHub/Requests/CreateWorkflowDispatchEventRequest.php
中,添加以下代码,以便我们能够逐步了解它
class CreateWorkflowDispatchEventRequest extends SaloonRequest{ use HasJsonBody; protected ?string $connector = GitHubConnector::class; public function defaultData(): array { return [ 'ref' => 'main', ]; } protected ?string $method = Saloon::POST; public function __construct( public string $owner, public string $repo, public string $workflow, ) {} public function defineEndpoint(): string { return "/repos/{$this->owner}/{$this->repo}/actions/workflows/{$this->workflow}/dispatches"; }}
我们正在设置连接器,并继承了 HasJsonBody
trait 以允许我们发送数据。该方法已设置为 POST
请求,因为我们想要发送数据。然后我们有一个构造函数,它接受 URL 的部分,这些部分构建了端点。最后,我们在 defaultData
中有一些默认数据,我们可以使用它来设置此 POST 请求的默认值。由于它是针对仓库的,我们可以在这里传递一个提交哈希或一个分支名称 - 所以我的默认值为 main
,因为这通常是我使用的生产分支。现在我们可以触发此端点来调度新的工作流事件,所以让我们创建一个控制台命令来控制它,以便我们能够从 CLI 运行它
php artisan make:command GitHub/CreateWorkflowDispatchEvent
现在让我们填写详细信息,然后我们可以逐步了解正在发生的事情
class CreateWorkflowDispatchEvent extends Command{ protected $signature = 'github:dispatch {owner : The owner or organisation.} {repo : The repository we are looking at.} {workflow : The ID of the workflow we want to dispatch.} {branch? : Optional: The branch name to run the workflow against.} '; protected $description = 'Create a new workflow dispatch event for a repository.'; public function handle(): int { $owner = (string) $this->argument('owner'); $repo = (string) $this->argument('repo'); $workflow = (string) $this->argument('workflow'); $request = new CreateWorkflowDispatchEventRequest( owner: $owner, repo: $repo, workflow: $workflow, ); $request->withTokenAuth( token: (string) config('services.github.token'), ); if ($this->hasArgument('branch')) { $request->setData( data: ['ref' => $this->argument('branch')], ); } $this->info( string: "Requesting a new workflow dispatch for {$owner}/{$repo} using workflow: {$workflow}", ); $response = $request->send(); if ($response->failed()) { throw $response->toException(); } $this->info( string: 'Request was accepted by GitHub', ); return self::SUCCESS; }}
所以和以前一样,我们有一个签名和一个描述,这次我们的签名有一个可选的分支,以防我们想要覆盖请求中的默认值。因此,在我们的 handle 方法中,我们可以简单地检查输入是否包含参数 'branch',如果是,我们可以解析它并设置请求的数据。然后,我们给 CLI 一些反馈,让用户知道我们在做什么 - 并发送请求。如果一切顺利,我们只需输出一条消息,告知用户 GitHub 接受了请求。但是,如果出现错误,我们希望抛出特定的异常,至少在开发期间。
最后一个请求的主要注意事项是,我们的工作流设置为通过添加一个新的 on
项目到工作流中,通过 webhook 触发
on: workflow_dispatch
就这样!我们正在使用 Saloon 和 Laravel 不仅列出仓库工作流,而且如果配置正确,我们还可以按需触发它们 :muscle
正如我在本教程开头所说,有很多方法可以处理 API 集成,但有一点是肯定的 - 使用 Saloon 使它变得干净、简单,而且使用起来也很愉快。
Laravel News 的技术作家,Treblle 的开发者倡导者。API 专家,经验丰富的 PHP/Laravel 工程师。YouTube 直播主.