Найти тему
Laravel Topsite Web

Работа со сторонними сервисами в laravel

Для этой статьи мы будем использовать Planetscale API. Planetscale — это сервис баз данных, который используется для операций чтения и записи.

Что будет делать наша интеграция? Представьте, что у нас есть приложение, которое позволяет нам управлять нашей инфраструктурой. Наши серверы проходят через Laravel Forge, а наша база данных находится в Planetscale. Нет чистого способа управления этим рабочим процессом, поэтому мы создали свой собственный. Для этого нам нужна интеграция.

Первоначально я сохранял интеграцию в app/Services, однако, поскольку мои приложения становились все более обширными и сложными, мне нужно было использовать пространство имен Services для внутренних служб, что привело к загрязненному пространству имен. Я перенес свои интеграции в app/Http/Integrations.

Теперь я мог бы использовать Saloon для интеграции API, но я хотел объяснить, как я делаю это без пакета. Если вам нужна интеграция API в 2023 году, я настоятельно рекомендую использовать Saloon.

Итак, давайте начнем с создания каталога для нашей интеграции. Вы можете использовать следующую команду bash:

mkdir app/Http/Integrations/Planetscale

Как только у нас появится каталог Planetscale, нам нужно создать способ подключения к нему. Еще одно соглашение об именовании, которое я взял из библиотеки Saloon, заключается в том, чтобы рассмотреть эти базовые классы как соединители, так как их цель состоит в том, чтобы позволить вам подключиться к определенному API.

Создайте новый класс под названием PlanetscaleConnector в каталоге app/Http/Integrations/Planetscale, и мы сможем конкретизировать, что нужно этому классу.

Поэтому мы должны зарегистрировать этот класс в нашем контейнере, чтобы разрешить его или построить фасад вокруг него. Мы могли бы зарегистрировать это "долго" в поставщике услуг — но мой последний подход заключается в том, чтобы эти разъемы зарегистрировались сами.

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, потому что мне не нужно искать провайдера и находить правильную привязку, среди многих других.

Вы заметите, что в настоящее время у нас нет ничего, передаваемого токену или базовым методам 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 [
//остальная конфигурация ваших служб
'planetscale' => [
'id' => env('PLANETSCALE_SERVICE_ID'),
'token' => env('PLANETSCALE_SERVICE_TOKEN'),
'url' => env('PLANETSCALE_URL'),
],
];

Теперь мы можем использовать их в нашем соединителе PlanetscaleConnector с помощью метода register.

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(),
),
);
}
}

Вам нужно отправить токены в Planetscale в следующем формате:service-id:service-token, поэтому мы не можем использовать метод по умолчанию withToken, так как он не позволяет нам настроить его так, как нам нужно.

Теперь, когда у нас есть базовый класс, мы можем начать думать о степени нашей интеграции. Мы должны сделать это при создании нашего токена сервиса, чтобы добавить правильные разрешения.

В нашем приложении мы хотим иметь возможность делать следующее:

  • Список баз данных.
  • Список регионов базы данных.
  • Список резервных копий базы данных.
  • Создайте резервную копию базы данных.
  • Удалить резервную копию базы данных.

Таким образом, мы можем сгруппировать их в две категории:

  1. Базы данных.
  2. Резервное копирование.

Давайте добавим два новых метода в наш соединитель, чтобы создать то, что нам нужно:

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. Они вернут новые классы ресурсов, проходящие через соединитель. Теперь логика может быть реализована в классах ресурсов, но позже мы должны добавить другой метод в наш соединитель.

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 и начать заполнять логику для метода list.

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()
{
//
}
}

Наш метод list принимает параметр organization для передачи через организацию в список баз данных. Затем мы используем это для отправки запроса на определенный URL-адрес через соединитель. Обертывание этого в инструкцию try-catch позволяет нам перехватывать потенциальные исключения из метода отправки соединителей. Наконец, мы можем вернуть коллекцию из метода для работы с ней в нашем приложении.

Мы можем более подробно ознакомиться с этим запросом, так как мы можем начать сопоставлять данные из массивов с чем-то более контекстуально полезным с помощью DTO.

Давайте быстро рассмотрим BackupResource, чтобы посмотреть больше, чем просто запрос на получение.

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-адресу нужен набор параметров, и нам нужно отправить тело запроса.

Я могу создать надежную и расширяемую интеграцию с третьими сторонами, используя этот подход.