在 Laravel 中使用第三方服务
发布日期:作者: Steve McDougall
大约两年前,我写了一篇关于如何在 Laravel 中使用第三方服务的教程。时至今日,它仍然是我网站上访问量最高的页面。然而,在过去的两年里,情况发生了变化,我决定重新审视这个主题。
我使用第三方服务已经很久了,我甚至不记得没有使用它们的时候是什么时候。作为一名初级开发者,我将 API 集成到 Joomla、Magento 和 WordPress 等其他平台中。现在,我主要将 API 集成到我的 Laravel 应用程序中,通过依赖其他服务来扩展业务逻辑。
本教程将描述我目前如何通常处理与 API 的集成。如果你读过我的上一篇教程,请继续阅读,因为一些内容已经改变,我认为这些改变是合理的。
让我们从一个 API 开始。我们需要一个可以集成的 API。我的最初教程是集成 PingPing,它是 Laravel 社区提供的一个优秀的运行状况监控解决方案。但是,这一次我想尝试一个不同的 API。
在本教程中,我们将使用 Planetscale API。Planetscale 是一款很棒的数据库服务,我在日常工作中使用它来使我的读写操作更接近我的用户。
我们的集成会做什么?想象一下,我们有一个应用程序,它允许我们管理我们的基础设施。我们的服务器通过 Laravel Forge 运行,我们的数据库在 Planetscale 上。没有一种干净的方式来管理这个工作流,所以我们创建了自己的方法。为此,我们需要一个或多个集成。
最初,我将我的集成保存在 app/Services
下;但是,随着我的应用程序越来越大越来越复杂,我需要使用 Services
命名空间来处理内部服务,导致命名空间变得很混乱。我已经将我的集成迁移到了 app/Http/Integrations
。这样做是有意义的,而且这是一个我从 Sam Carrè 的 Saloon 中学到的技巧。
现在我可以使用 Saloon 来进行我的 API 集成,但我想要解释一下如何在没有包的情况下进行集成。如果在 2023 年需要 API 集成,我强烈建议使用 Saloon。它非常棒!
那么,让我们从创建一个集成目录开始。你可以使用以下 bash 命令
mkdir app/Http/Integrations/Planetscale
创建 Planetscale 目录后,我们需要创建一种连接到它的方式。我从 Saloon 库中学到的另一个命名约定是将这些基类视为连接器——因为它们的目的是允许你连接到特定的 API 或第三方。
在 app/Http/Integrations/Planetscale
目录中创建一个名为 PlanetscaleConnector
的新类,我们可以详细说明这个类需要什么,这将很有趣。
因此,我们必须将这个类注册到我们的容器中,以便解析它或在其周围构建一个外观。我们可以在服务提供者中以“长”的方式注册它——但我的最新方法是让这些连接器自行注册——有点像...
declare(strict_types=1); namespace App\Http\Integrations\Planetscale; use Illuminate\Contracts\Foundation\Application;use Illuminate\Http\Client\PendingRequest;use Illuminate\Support\Facades\Http; final readonly class PlanetscaleConnector{ public function __construct( private PendingRequest $request, ) {} public static function register(Application $app): void { $app->bind( abstract: PlanetscaleConnector::class, concrete: fn () => new PlanetscaleConnector( request: Http::baseUrl( url: '', )->timeout( seconds: 15, )->withHeaders( headers: [], )->asJson()->acceptJson(), ), ); }}
因此,这里的想法是,关于这个类如何注册到容器中的所有信息都存在于类本身中。服务提供者只需要调用类上的静态注册方法!这为我节省了很多时间,因为我无需在许多 API 集成中搜索提供者并找到正确的绑定。我只需要去相关的类,所有信息都在我面前。
你会注意到,目前我们没有将任何东西传递给请求中的 token 或 base url 方法。接下来让我们解决这个问题。你可以在你的 Planetscale 帐户中获取这些信息。
在你的 .env
文件中创建以下记录。
PLANETSCALE_SERVICE_ID="your-service-id-goes-here"PLANETSCALE_SERVICE_TOKEN="your-token-goes-here"PLANETSCALE_URL="https://api.planetscale.com/v1"
接下来,需要将这些信息拉到应用程序的配置中。它们都属于 config/services.php
,因为这是通常配置第三方服务的 地方。
return [ // the rest of your services config 'planetscale' => [ 'id' => env('PLANETSCALE_SERVICE_ID'), 'token' => env('PLANETSCALE_SERVICE_TOKEN'), 'url' => env('PLANETSCALE_URL'), ],];
现在我们可以在 PlanetscaleConnector
中的注册方法中使用这些信息。
declare(strict_types=1); namespace App\Http\Integrations\Planetscale; use Illuminate\Contracts\Foundation\Application;use Illuminate\Http\Client\PendingRequest;use Illuminate\Support\Facades\Http; final readonly class PlanetscaleConnector{ public function __construct( private PendingRequest $request, ) {} public static function register(Application $app): void { $app->bind( abstract: PlanetscaleConnector::class, concrete: fn () => new PlanetscaleConnector( request: Http::baseUrl( url: config('services.planetscale.url'), )->timeout( seconds: 15, )->withHeaders( headers: [ 'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'), ], )->asJson()->acceptJson(), ), ); }}
你需要以 service-id:service-token
的格式将令牌发送到 Planetscale,所以我们不能使用默认的 withToken
方法,因为它不允许我们根据需要自定义它。
现在我们已经创建了一个基本的类,我们可以开始考虑集成的范围。在创建我们的服务令牌时,我们必须这样做,以添加正确的权限。在我们的应用程序中,我们希望能够执行以下操作:列出数据库。列出数据库区域。列出数据库备份。创建数据库备份。删除数据库备份。
因此,我们可以考虑将这些操作分组到两个类别中:数据库。备份。
让我们向连接器添加两个新方法来创建我们需要的内容
declare(strict_types=1); namespace App\Http\Integrations\Planetscale; use App\Http\Integrations\Planetscale\Resources\BackupResource;use App\Http\Integrations\Planetscale\Resources\DatabaseResource;use Illuminate\Contracts\Foundation\Application;use Illuminate\Http\Client\PendingRequest;use Illuminate\Support\Facades\Http; final readonly class PlanetscaleConnector{ public function __construct( private PendingRequest $request, ) {} public function databases(): DatabaseResource { return new DatabaseResource( connector: $this, ); } public function backups(): BackupResource { return new BackupResource( connector: $this, ); } public static function register(Application $app): void { $app->bind( abstract: PlanetscaleConnector::class, concrete: fn () => new PlanetscaleConnector( request: Http::baseUrl( url: config('services.planetscale.url'), )->timeout( seconds: 15, )->withHeaders( headers: [ 'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'), ], )->asJson()->acceptJson(), ), ); }}
如你所见,我们创建了两个新方法,databases
和 backups
。它们将返回新的资源类,并传递连接器。逻辑现在可以在资源类中实现,但我们稍后需要向连接器添加另一个方法。
<?php declare(strict_types=1); namespace App\Http\Integrations\Planetscale\Resources; use App\Http\Integrations\Planetscale\PlanetscaleConnector; final readonly class DatabaseResource{ public function __construct( private PlanetscaleConnector $connector, ) {} public function list() { // } public function regions() { // }}
这是我们的 DatabaseResource
;我们现在已经为我们要实现的方法创建了占位符。你也可以对 BackupResource
做同样的事情。它看起来会有点类似。
因此,结果可以在数据库列表中进行分页。但是,我在这里不会处理这个问题——我建议使用 Saloon,因为它对分页结果的实现非常棒。在这个例子中,我们不会担心分页。在我们填充 DatabaseResource
之前,我们需要向 PlanetscaleConnector
添加另一个方法来很好地发送请求。为此,我使用的是我名为 juststeveking/http-helpers
的包,它有一个枚举,包含我使用的所有典型 HTTP 方法。
public function send(Method $method, string $uri, array $options = []): Response{ return $this->request->send( method: $method->value, url: $uri, options: $options, )->throw();}
现在我们可以回到我们的 DatabaseResource
并开始填充列表方法的逻辑。
declare(strict_types=1); namespace App\Http\Integrations\Planetscale\Resources; use App\Http\Integrations\Planetscale\PlanetscaleConnector;use Illuminate\Support\Collection;use JustSteveKing\HttpHelpers\Enums\Method;use Throwable; final readonly class DatabaseResource{ public function __construct( private PlanetscaleConnector $connector, ) {} public function list(string $organization): Collection { try { $response = $this->connector->send( method: Method::GET, uri: "/organizations/{$organization}/databases" ); } catch (Throwable $exception) { throw $exception; } return $response->collect('data'); } public function regions() { // }}
我们的列表方法接受参数 organization
来传递组织,以列出数据库。然后,我们使用它通过连接器向特定 URL 发送请求。将它包装在 try-catch 语句中,可以让我们捕获连接器 send 方法可能发生的异常。最后,我们可以从方法返回一个集合,以便在我们的应用程序中使用它。
我们可以更详细地了解这个请求,因为我们可以开始将数据从数组映射到使用 DTO 时更有语义意义的东西。我曾在 这里 讨论过这个问题,所以这里不再重复。
让我们快速看一下 BackupResource
,看看不仅仅是 get 请求。
declare(strict_types=1); namespace App\Http\Integrations\Planetscale\Resources; use App\Http\Integrations\Planetscale\Entities\CreateBackup;use App\Http\Integrations\Planetscale\PlanetscaleConnector;use JustSteveKing\HttpHelpers\Enums\Method;use Throwable; final readonly class BackupResource{ public function __construct( private PlanetscaleConnector $connector, ) {} public function create(CreateBackup $entity): array { try { $response = $this->connector->send( method: Method::POST, uri: "/organizations/{$entity->organization}/databases/{$entity->database}/branches/{$entity->branch}", options: $entity->toRequestBody(), ); } catch (Throwable $exception) { throw $exception; } return $response->json('data'); }}
我们的创建方法接受一个实体类,我使用它来将数据通过应用程序传递到需要的地方。当 URL 需要一组参数并且我们需要通过请求主体发送数据时,这很有用。
我还没有介绍测试,但我写了一篇关于如何 使用 PestPHP 测试 JSON:API 端点 的教程,其中包含类似的概念,用于测试类似的集成。
使用这种方法,我可以创建可靠且可扩展的第三方集成。它被分成逻辑部分,所以我可以处理大量的逻辑。通常我会拥有更多的集成,因此可以将一些逻辑提取到特性中,并在集成之间继承行为。
技术作家,就职于 Laravel 新闻,开发者倡导者,就职于 Treblle。API 专家,经验丰富的 PHP/Laravel 工程师。YouTube 直播主.