在Laravel中使用Saloon进行API集成

186 阅读14分钟

我们都有过这样的经历, 我们想在Laravel中与第三方API集成, 我们问自己 "我应该怎么做?".当涉及到API集成时, 我并不陌生, 但每次我还是想知道什么是最好的方法.Sam Carré在2022年早期建立了一个名为Saloon软件包,可以让我们的API集成变得非常棒。然而,这篇文章将是非常不同的,这将是一个关于你如何使用它从头开始建立一个集成的步骤。

像所有伟大的事情一样, 它开始于一个laravel new ,然后从那里开始,所以让我们开始吧。现在,当涉及到安装Laravel时,你可以使用laravel安装程序或composer - 这一部分取决于你。如果你可以的话,我推荐使用安装程序,因为它提供了简单的选项,不仅仅是创建一个项目。创建一个新的项目并在你选择的代码编辑器中打开它。一旦我们到了那里,我们就可以开始了。

我们要建立什么?我很高兴你问了这个问题。我们将建立一个与GitHub API的集成,以获得一个 repo可用的工作流程的列表。现在,如果你像我一样,花很多时间在命令行上,这可能是超级有用的。你正在开发一个应用,你向一个分支推送修改,或者创建一个PR--它经过一个工作流,可能正在运行许多其他的事情。知道这个工作流程的状态有时对你接下来的工作有很大的影响。这个功能完成了吗?我们的工作流运行有问题吗?我们的测试或静态分析是否通过?所有这些事情,你通常会等待,并在GitHub上检查回购,以查看状态。这个集成将允许你运行一个artisan命令,获得一个 repo的可用工作流列表,并允许你触发一个新的工作流运行。

所以现在, composer应该已经完成了它的工作并安装了一个完美的起点, 一个Laravel应用程序。接下来,我们需要安装Saloon - 但我们要确保我们安装的是laravel版本,所以在你的终端运行以下程序。

1composer require sammyjo20/saloon-laravel

就这样, 我们已经离更简单的集成更近了一步.如果你在这个阶段有任何问题, 请确保你同时检查你所使用的Laravel和PHP版本, 因为Saloon至少需要Laravel 8和PHP 8!

所以, 现在我们已经安装了Saloon,我们需要创建一个新的类。在Saloon的术语中,这些都是 "连接器",连接器所做的就是创建一个专注于对象的方式来表示 - 这个API是通过这个类连接的。有一个方便的artisan命令可以让你创建这些,所以运行下面的artisan命令来创建一个GitHub连接器。

1php artisan saloon:connector GitHub GitHubConnector

这个命令分为两个部分,第一个参数是你要创建的集成,第二个参数是你要创建的连接器的名称。这意味着你可以为一个集成创建多个连接器--这给了你很大的控制权,如果你需要,可以用很多不同的方式进行连接。

这将在app/Http/Integrations/GitHub/GitHubConnector.php ,为你创建一个新的类,让我们看一下这个类,并了解发生了什么。

我们看到的第一件事是我们的连接器扩展了SaloonConnector ,这将使我们的连接器能够在没有大量模板代码的情况下工作。然后我们继承了一个名为AcceptsJson 的特性。现在,如果我们看一下Saloon的文档,我们知道这是一个插件。这基本上是给我们的请求添加一个头,告诉第三方API我们想接受JSON响应。我们看到的下一件事是,我们有一个方法来定义我们的连接器的基本URL - 所以让我们把我们的加入进去。

1public function defineBaseUrl(): string2{3    return 'https://api.github.com';4}

很好,很干净,我们甚至可以更进一步,这样我们就可以减少在我们的应用程序中悬挂的松散字符串--所以让我们看看我们可以如何做到这一点。在你的config/services.php 文件中添加一个新的服务记录。

1'github' => [2    'url' => env('GITHUB_API_URL', 'https://api.github.com'),3]

这将允许我们在不同的环境中覆盖它--给我们一个更好、更可测试的解决方案。在本地,我们甚至可以使用GitHub的OpenAPI规范来模拟GitHub的API,并对其进行测试以确保其正常工作。然而,本教程是关于Saloon的,所以我偏离了主题......现在让我们重构我们的基本URL方法以使用配置。

1public function defineBaseUrl(): string2{3    return (string) config('services.github.url');4}

正如你所看到的,我们现在正在从我们的配置中获取新添加的记录--为了类型安全,将其转换为一个字符串--config() ,返回一个混合的结果,所以如果可以的话,我们希望在这方面严格执行。

接下来我们有默认的头文件和默认的配置,现在我不打算担心默认的头文件,因为我们将在一小段时间内接近auth本身。但配置是我们可以为我们的集成定义Guzzle选项的地方,因为Saloon在后台使用Guzzle。现在让我们设置超时并继续前进,但请随意花一些时间来配置你认为合适的东西。

1public function defaultConfig(): array2{3    return [4        'timeout' => 30,5    ];6}

我们现在已经把我们的连接器配置成我们现在需要的样子,如果我们发现有什么需要添加的东西,我们可以在以后再回来。下一步是开始考虑我们要发送的请求。如果我们看一下 GitHub Actions API 的文档,我们有很多选择,我们将从列出某个特定仓库的工作流程开始:/repos/{owner}/{repo}/actions/workflows 。运行下面的 artisan 命令,创建一个新的请求。

1php artisan saloon:request GitHub ListRepositoryWorkflowsRequest

同样,第一个参数是集成,第二个参数是我们要创建的请求的名称。我们需要确保为我们要创建的请求命名集成,这样它就会住在正确的地方,然后我们需要给它一个名字。我把我的名字叫做ListRepositoryWorkflowsRequest ,因为我喜欢描述性的命名方法--然而,请随意调整,以适应你喜欢的命名方式,因为这里没有真正的错误方法。这将创建一个新的文件供我们查看。 app/Http/Integrations/GitHub/Requests/ListRepositoryWorkflowsRequest.php - 现在让我们来看看这个。

我们又一次在这里扩展了一个库类,这次是SaloonRequest ,这是可以预料的。然后我们有一个连接器属性和一个方法。如果我们需要,我们可以改变这个方法--但是默认的GET 是我们现在需要的。然后,我们有一个定义端点的方法。重构你的请求类,使其看起来像下面的例子。

 1class ListRepositoryWorkflowsRequest extends SaloonRequest 2{ 3    protected ?string $connector = GitHubConnector::class; 4  5    protected ?string $method = Saloon::GET; 6  7    public function __construct( 8        public string $owner, 9        public string $repo,10    ) {}11 12    public function defineEndpoint(): string13    {14        return "/repos/{$this->owner}/{$this->repo}/actions/workflows";15    }16}

我们所做的是添加一个构造函数,接受 repo 和 owner 作为参数,然后我们可以在定义端点的方法中使用。我们还将连接器设置为我们之前创建的GitHubConnector 。所以我们有了一个可以发送的请求,我们可以从集成中抽出一小步,考虑一下控制台命令。

如果你以前没有在Laravel中创建一个控制台命令, 请确保你检查一下文档, 它非常好。运行下面的artisan命令来创建这个集成的第一个命令:

1php artisan make:command GitHub/ListRepositoryWorkflows

这将创建以下文件:app/Console/Commands/GitHub/ListRespositoryWorkflows.php.我们现在可以开始使用我们的命令,使其发送请求并获得我们关心的数据。当涉及到控制台命令时,我所做的第一件事就是考虑签名。我想让这个命令如何被调用?它需要解释它在做什么,但它也需要让人记住。我打算把我的叫做github:workflows ,因为它能很好地解释我的意思。我们也可以给我们的控制台命令添加一个描述,这样在浏览可用的命令时,就能更好地解释其目的。"按仓库名称从GitHub获取工作流列表"。

最后,我们进入命令的处理方法,也就是我们实际需要做的部分。在我们的例子中,我们将发送一个请求,获得一些数据,并以某种方式显示这些数据。然而,在我们做这些之前,有一件事我们到现在为止还没有做。那就是认证。对于每一个API集成,认证都是一个关键的方面--我们需要API不仅知道我们是谁,还需要知道我们确实被允许提出这个请求。如果你进入GitHub设置,点击开发者设置和个人访问令牌,你就可以在这里生成自己的访问令牌。我建议使用这种方法,而不是去做一个完整的OAuth应用。我们不需要OAuth,我们只需要用户能够访问他们需要的东西。

一旦你有了你的访问令牌,我们需要把它添加到我们的.env 文件,并确保我们可以通过我们的配置来拉动它。

1GITHUB_API_TOKEN=ghp_loads-of-letters-and-numbers-here

我们现在可以在github下的config/services.php ,扩展我们的服务,添加这个令牌。

1'github' => [2    'url' => env('GITHUB_API_URL', 'https://api.github.com'),3    'token' => env('GITHUB_API_TOKEN'),4]

现在我们有一个很好的方法来加载这个令牌,我们可以回到我们的控制台命令中去了我们需要修改我们的签名,允许我们接受所有者和版本库作为参数。

 1class ListRepositoryWorkflows extends Command 2{ 3    protected $signature = 'github:workflows 4        {owner : The owner or organisation.} 5		{repo : The repository we are looking at.} 6	'; 7  8    protected $description = 'Fetch a list of workflows from GitHub by the repository name.'; 9 10    public function handle(): int11    {12        return 0;13    }14}

现在我们可以把注意力转移到处理方法上。

1public function handle(): int2{3    $request = new ListRepositoryWorkflowsRequest(4        owner: $this->argument('owner'),5        repo: $this->argument('repo'),6    );7 8    return self::SUCCESS;9}

在这里,我们开始通过将参数直接传入Request本身来建立我们的请求,然而我们可能想做的是创建一些本地变量来提供一些控制台反馈。

 1public function handle(): int 2{ 3    $owner = (string) $this->argument('owner'); 4    $repo = (string) $this->argument('repo'); 5  6    $request = new ListRepositoryWorkflowsRequest( 7        owner: $owner, 8        repo: $repo, 9    );10 11    $this->info(12        string: "Fetching workflows for {$owner}/{$repo}",13    );14 15    return self::SUCCESS;16}

这样我们就有了一些对用户的反馈,当涉及到一个控制台命令时,这一点总是很重要。现在我们需要添加我们的认证令牌并实际发送请求。

 1public function handle(): int 2{ 3    $owner = (string) $this->argument('owner'); 4    $repo = (string) $this->argument('repo'); 5  6    $request = new ListRepositoryWorkflowsRequest( 7        owner: $owner, 8        repo: $repo, 9    );10 11    $request->withTokenAuth(12        token: (string) config('services.github.token'),13    );14 15    $this->info(16        string: "Fetching workflows for {$owner}/{$repo}",17    );18 19    $response = $request->send();20 21    return self::SUCCESS;22}

如果你修改上面的内容,在$response->json() ,只是暂时的,做一个dd() 。然后运行该命令。

1php artisan github:workflows laravel laravel

这将得到laravel/laravel repo的工作流程列表。我们的命令将允许你在任何公共仓库工作,如果你想让它更具体,你可以建立一个你想检查的仓库的选项列表,而不是接受参数 - 但这部分由你决定。在本教程中,我将专注于更广泛更开放的用例。

现在,我们从GitHub API得到的响应很好,信息量也很大,但它需要转化才能显示,如果我们孤立地看它,就没有背景。相反,我们将为我们的请求添加另一个插件,这将允许我们将响应转化为DTO(域传输对象),这是一个很好的处理方式。它将允许我们放弃我们习惯于从API中获得的灵活的数组,而得到一些更具有上下文意识的东西。让我们为工作流创建一个DTO,创建一个新的文件:app/Http/Integrations/GitHub/DataObjects/Workflow.php ,并在其中添加以下代码。

 1class Workflow 2{ 3    public function __construct( 4        public int $id, 5        public string $name, 6        public string $state, 7    ) {} 8  9    public static function fromSaloon(array $workflow): static10    {11        return new static(12            id: intval(data_get($workflow, 'id')),13            name: strval(data_get($workflow, 'name')),14            state: strval(data_get($workflow, 'state')),15        );16    }17 18    public function toArray(): array19    {20        return [21            'id' => $this->id,22            'name' => $this->name,23            'state' => $this->state,24        ];25    }26}

我们有一个构造函数,它包含了我们想要显示的工作流的重要部分;一个fromSaloon 方法,它将把一个数组从沙龙响应中转化为一个新的DTO;还有一个to数组方法,当我们需要时将DTO显示为一个数组。在我们的ListRepositoryWorkflowsRequest ,我们需要继承一个新的trait并添加一个新方法。

 1class ListRepositoryWorkflowsRequest extends SaloonRequest 2{ 3    use CastsToDto; 4  5    protected ?string $connector = GitHubConnector::class; 6  7    protected ?string $method = Saloon::GET; 8  9    public function __construct(10        public string $owner,11        public string $repo,12    ) {}13 14    public function defineEndpoint(): string15    {16        return "/repos/{$this->owner}/{$this->repo}/actions/workflows";17    }18 19    protected function castToDto(SaloonResponse $response): Collection20    {21        return (new Collection(22            items: $response->json('workflows'),23        ))->map(function ($workflow): Workflow {24            return Workflow::fromSaloon(25                workflow: $workflow,26            );27        });28    }29}

我们继承了CastsToDto 特质,它允许这个请求在响应上调用dto 方法,然后我们添加一个castToDto 方法,在这里我们可以控制如何转换。我们希望这个方法能够返回一个新的集合,因为有不止一个工作流,使用响应体的工作流部分。然后,我们将集合中的每个项目进行映射,并将其变成一个DTO。现在我们既可以这样做,也可以这样做,我们用DTO建立我们的集合。

 1protected function castToDto(SaloonResponse $response): Collection 2{ 3	return new Collection( 4		items: $response->collect('workflows')->map(fn ($workflow) => 5			Workflow::fromSaloon( 6				workflow: $workflow 7			), 8		) 9	);10}

你可以在这里选择最适合你的方式。我个人更喜欢第一种方法,因为我喜欢一步步看清逻辑,但两种方法都没有错--选择权在你。现在回到命令上,我们现在需要考虑我们要如何显示这些信息。

 1public function handle(): int 2{ 3    $owner = (string) $this->argument('owner'); 4    $repo = (string) $this->argument('repo'); 5  6    $request = new ListRepositoryWorkflowsRequest( 7        owner: $owner, 8        repo: $repo, 9    );10 11    $request->withTokenAuth(12        token: (string) config('services.github.token'),13    );14 15    $this->info(16        string: "Fetching workflows for {$owner}/{$repo}",17    );18 19    $response = $request->send();20 21    if ($response->failed()) {22        throw $response->toException();23	}24 25    $this->table(26        headers: ['ID', 'Name', 'State'],27        rows: $response28			->dto()29			->map(fn (Workflow $workflow) =>30				  $workflow->toArray()31			)->toArray(),32    );33 34    return self::SUCCESS;35}

因此,我们创建一个带有标题的表格,然后对于行,我们希望得到响应的DTO,我们将映射返回的集合,将每个DTO投回给一个数组来显示。从响应数组到DTO再到数组,这似乎是反直觉的,但这将做的是执行类型,以便ID、名称和状态在预期时总是在那里,而且不会产生任何有趣的结果。它允许一致性,而普通的响应数组可能不具备这种一致性,如果我们想的话,我们可以把它变成一个价值对象,在那里我们有行为附加。如果我们现在运行我们的命令,我们现在应该看到一个漂亮的表格输出,这比几行字符串更容易阅读。

1php artisan github:workflows laravel laravel
1Fetching workflows for laravel/laravel2+----------+------------------+--------+3| ID       | Name             | State  |4+----------+------------------+--------+5| 12345678 | pull requests    | active |6| 87654321 | Tests            | active |7| 18273645 | update changelog | active |8+----------+------------------+--------+

最后,仅仅列出这些工作流程是很好的--但让我们以科学的名义再进一步。假设你在对你的一个仓库运行这个命令,你想手动运行更新日志?或者你想用你的实时生产服务器或任何你能想到的事件在cron上触发这个命令?我们可以将更新日志设置为每天午夜运行一次,这样我们就可以在更新日志中得到每日回顾或任何我们可能想要的东西。让我们创建另一个控制台命令来创建一个新的工作流调度事件。

1php artisan saloon:request GitHub CreateWorkflowDispatchEventRequest

在这个新文件中app/Http/Integrations/GitHub/Requests/CreateWorkflowDispatchEventRequest.php ,添加以下代码,这样我们就可以走完它。

 1class CreateWorkflowDispatchEventRequest extends SaloonRequest 2{ 3    use HasJsonBody; 4  5    protected ?string $connector = GitHubConnector::class; 6  7    public function defaultData(): array 8    { 9        return [10            'ref' => 'main',11        ];12    }13 14    protected ?string $method = Saloon::POST;15 16    public function __construct(17        public string $owner,18        public string $repo,19        public string $workflow,20    ) {}21 22    public function defineEndpoint(): string23    {24        return "/repos/{$this->owner}/{$this->repo}/actions/workflows/{$this->workflow}/dispatches";25    }26}

我们正在设置连接器,并继承HasJsonBody 特质以允许我们发送数据。该方法被设置为POST 请求,因为我们想发送数据。然后我们有一个构造函数,接受建立端点的URL部分。最后,我们在defaultData 里面有穹顶默认数据,我们可以用它来为这个帖子请求设置默认值。由于它是针对 repo 的,我们可以在这里传递一个提交哈希值或一个分支名称--所以我把默认值设置为main ,因为这是我通常所说的生产分支。现在我们可以触发这个端点来分配一个新的工作流事件,所以让我们创建一个控制台命令来控制它,这样我们就可以从CLI运行它。

1php artisan make:command GitHub/CreateWorkflowDispatchEvent

现在,让我们填入细节,然后我们可以走过正在发生的事情。

 1class CreateWorkflowDispatchEvent extends Command 2{ 3    protected $signature = 'github:dispatch 4        {owner : The owner or organisation.} 5		{repo : The repository we are looking at.} 6		{workflow : The ID of the workflow we want to dispatch.} 7		{branch? : Optional: The branch name to run the workflow against.} 8	'; 9 10    protected $description = 'Create a new workflow dispatch event for a repository.';11 12    public function handle(): int13    {14        $owner = (string) $this->argument('owner');15        $repo = (string) $this->argument('repo');16        $workflow = (string) $this->argument('workflow');17 18        $request = new CreateWorkflowDispatchEventRequest(19            owner: $owner,20            repo: $repo,21            workflow: $workflow,22        );23 24        $request->withTokenAuth(25            token: (string) config('services.github.token'),26        );27 28		if ($this->hasArgument('branch')) {29			$request->setData(30				data: ['ref' => $this->argument('branch')],31			);32        }33 34        $this->info(35            string: "Requesting a new workflow dispatch for {$owner}/{$repo} using workflow: {$workflow}",36        );37 38        $response = $request->send();39 40        if ($response->failed()) {41            throw $response->toException();42        }43 44        $this->info(45            string: 'Request was accepted by GitHub',46        );47 48        return self::SUCCESS;49    }50}

像以前一样,我们有一个签名和一个描述,这次我们的签名有一个可选的分支,以备我们想覆盖请求中的默认值。所以在我们的处理方法中,我们可以简单地检查输入是否有参数 "分支",如果有,我们可以解析这个参数并为请求设置数据。然后我们给CLI一点反馈,让用户知道我们在做什么--并发送请求。如果这时一切顺利,我们可以简单地输出一条消息,告知用户 GitHub 接受了请求。但是如果出了问题,我们要抛出特定的异常,至少在开发过程中是这样。

最后一个请求的主要注意事项是,我们的工作流程被设定为通过在工作流程中添加一个新的on 项目,由网络钩子触发。

1on: workflow_dispatch

就是这样!我们使用Saloon和Laravel不仅可以列出存储库的工作流程,而且如果配置正确,我们还可以触发它们按需运行:肌肉。

正如我在本教程开始时所说的,有很多方法来处理API集成,但有一点是肯定的--使用Saloon使它变得干净和简单,而且使用起来也相当愉快。

存档在。

教程

JustSteveKing

冯小刚(JustSteveKing)

Laravel新闻合作伙伴

  • Kirschbaum-dark.png
  • tighten-partner.png
  • loadforge-400x100.png

Laravel新闻合作伙伴

  • tighten-partner.png
  • loadforge-400x100.png
  • Kirschbaum-dark.png