Работа со сторонними сервисами в Laravel
Итак, я так долго работаю со сторонними сервисами, что не могу вспомнить, когда не работал с ними. Будучи Junior Developer, я интегрировал API в другие платформы, такие как Joomla, Magneto и WordPress. Теперь он в основном интегрируется в мои Laravel приложения для расширения бизнес-логики за счёт использования других сервисов.
В этом руководстве будет описано, как я обычно подхожу к интеграции с API сегодня. Если вы читали моё предыдущее руководство, продолжайте читать, так как некоторые вещи изменились — по тому, что я считаю уважительными причинами.
Начнём с API. Нам нужен API для интеграции. В первоначальном руководстве была интеграция с PingPing, отличным решением для мониторинга времени безотказной работы от Laravel сообщества. Однако на этот раз я хочу попробовать другой API.
В этом руководстве мы будем использовать Planetscale API. Planetscale — это невероятный сервис баз данных, который я использую, чтобы сделать операции чтения и записи ближе к пользователям в повседневной работе.
Что даст эта интеграция? Представьте, что у нас есть приложение, которое позволяет управлять инфраструктурой. Серверы работают через Laravel Forge, а база данных на Planetscale. Не существует простого процесса управления этим рабочим процессом, поэтому мы создадим свой. Для этого нужна интеграция или две.
Изначально я хранил свои интеграции в app/Services
. Однако поскольку мои приложения стали более обширными и сложными, мне пришлось использовать пространство имён Services
для внутренних служб, что привело к загрязнению пространства имён. Я переместил свои интеграции в app/Http/Integrations
. Это имеет смысл, и это трюк, который я почерпнул из Saloon Сэма Карре.
Теперь я могу использовать Saloon для своей API-интеграции, но я хотел бы объяснить, как я делаю это без пакета. Если вам нужна интеграции API в 2023 году, я настоятельно рекомендую использовать Saloon. Он более чем удивительный!
Итак, начнём с создания каталога для нашей интеграции. Вы можете использовать следующую команду:
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 [
// the rest of your services config
'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
по умолчанию, поскольку он не позволяет настраивать его так, как нам нужно.
Теперь, когда у нас создан базовый класс, можно начать думать о степени интеграции. Мы должны сделать это при создании нашего токена сервиса, чтобы добавить правильные разрешения. В приложении мы хотим иметь возможность делать следующее:
- Получать список баз данных.
- Получать список регионов баз данных.
- Получать список резервных копий баз данных.
- Создавать резервные копии баз данных.
- Удалять резервные копии баз данных.
Таким образом, мы можем разделить их на две категории:
- Базы данных.
- Резервные копии.
Давайте добавим в наш коннектор два новых метода, чтобы создать то, что нам нужно:
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
и начать заполнять логику для метода 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
, мы можем перехватывать потенциальные исключения из метода send
коннекторов. Наконец, мы можем вернуть коллекцию из метода, чтобы работать с ней в приложении.
Мы можем углубиться в этот запрос, так как мы можем начать сопоставлять данные из массивов с чем-то более контекстуально полезным, используя 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');
}
}
Метод create
принимает класс сущности, который я использую для передачи данных через приложение там, где это необходимо. Это полезно, когда для URL-адреса требуется набор параметров, и нам нужно отправлять тело запроса.
Я не стал рассматривать тестирование, так как написал руководство о том, как тестировать конечные точки JSON:API с PestPHP, в котором есть аналогичные концепции для тестирования такой интеграции.
Используя этот подход, я могу создавать надёжные и расширяемые интеграции со сторонними сервисами. Он разбит на логические части, поэтому я могу справиться с объёмом логики. Обычно у меня бывает больше интеграций, поэтому часть логики можно разделить и извлечь в трейты для наследования поведения между интеграциями.