Итак, чуть более двух лет назад я написал учебник о том, как вы должны работать со сторонними сервисами в Laravel. По сей день это самая посещаемая страница на моем сайте. Однако за последние два года все изменилось, и я решил снова подойти к этой теме.
Так что я так долго работаю со сторонними сервисами, что не могу вспомнить, когда меня не было. Как младший разработчик, я интегрировал API в другие платформы, такие как Joomla, Magento и WordPress. Теперь он в основном интегрируется в мои приложения Laravel, чтобы расширить бизнес-логику, опираясь на другие сервисы.
В этом руководстве будет описано, как я обычно подхожу к интеграции с API сегодня. Если вы прочитали мой предыдущий урок, продолжайте читать, так как несколько вещей изменились - по тому, что я считаю вескими причинами.
Начнем с API. Нам нужен API для интеграции. Моим первоначальным руководством была интеграция с PingPing, отличным решением для мониторинга времени безотказной работы от сообщества Laravel. Однако на этот раз я хочу попробовать другой API.
В этом уроке мы будем использовать API Planetscale. Planetscale — это невероятный сервис баз данных, который я использую, чтобы приблизить мои операции чтения и записи к моим пользователям в повседневной работе.
Что даст наша интеграция? Представьте, что у нас есть приложение, которое позволяет нам управлять нашей инфраструктурой. Наши серверы работают через Laravel Forge, а наша база данных находится на Planetscale. Не существует четкого способа управления этим рабочим процессом, поэтому мы создали свой собственный. Для этого нам нужна одна или две интеграции.
Первоначально я держал свои интеграции под app/Services
контролем, однако, поскольку мои приложения стали более обширными и сложными, мне пришлось использовать Services
пространство имен для внутренних служб, что привело к загрязненному пространству имен. Я перенес свои интеграции в app/Http/Integrations
. Это имеет смысл, и это трюк, который я позаимствовал в Saloon Сэма Карре.
Теперь я мог бы использовать 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 [
// 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(),
),
);
}
}
Вам необходимо отправить токены в Planetscale в следующем формате: , поэтому мы не можем использовать метод по умолчаниюwithToken
, поскольку он не позволяет нам настроить его так, service-id:service-token
как нам нужно.
Теперь, когда у нас есть базовый класс, мы можем начать думать о степени нашей интеграции. Мы должны сделать это при создании нашего служебного токена, чтобы добавить правильные разрешения. В нашем приложении мы хотим иметь возможность делать следующее:
Список баз данных.
Список областей базы данных.
Список резервных копий баз данных.
Создайте резервную копию базы данных.
Удалите резервную копию базы данных.
Итак, мы можем сгруппировать их в две категории:
Баз данных.
Резервные копии.
Давайте добавим два новых метода в наш коннектор, чтобы создать то, что нам нужно:
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()
{
//
}
}
Наш метод списка принимает параметр 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');
}
}
Наш метод create принимает класс сущностей, который я использую для передачи данных через приложение там, где это необходимо. Это полезно, когда URL-адресу нужен набор параметров, и нам нужно отправить тело запроса.
Я не рассматривал тестирование здесь, но я написал учебник о том, как тестировать конечные точки JSON: API с помощью PestPHP здесь, в котором будут аналогичные концепции для тестирования интеграции, подобной этой.
Используя этот подход, я могу создавать надежные и расширяемые интеграции с третьими сторонами. Он разбит на логические части, поэтому я могу справиться с количеством логики. Как правило, у меня больше интеграций, поэтому часть этой логики может быть разделена и извлечена в черты, чтобы унаследовать поведение между интеграциями.