Saloon - универсальный инструмент для интеграции по API
В жизни каждого программиста возникает тот момент когда необходимо выбрать рекурсию или цикл. Почти с большей вероятностью в жизни каждого разработчика стоит выбор между rm
или rm -f
в консоли. Но! Без сомнений для каждого web-разработчика был опыт в взаимодействия по API с внешними системами или с другой командой. Проще говоря, у вас есть API и вам надо написать код – который будет принимать данные и отправлять их.
Так вот … у каждого web ремесленника, спустя годы может выработаться свое видение как реализовывать такие вещи и сегодня я хочу добавить в общий котел инструментов – библиотеку для интеграции с API и дать краткое представление об этой “альтернативе”.
Барабанная дробь… Тишина … кулисы открылись и вот он: Saloon !
Пакет, который поможет вам сделать свой SDK или интеграцию с внешней системой. Что собственного в нем такого интересного – рассмотрим с “высоты птичьего полета”.
Под капотом у библиотеки используется Guzzle, что не удивительно.
Структура
Первое что стоит выделить – это структура всей “этой истории”. Структура врочем важна как и во многих вещах в коде.(и не только в коде кстати 😊).
Например, в вашем любимом фреймворке есть структура(папки, названия классов и т.д) – если вы им пользуетесь то другой разработчик ожидает ее увидеть(так конечно не всегда – но часто должно быть эх…). В этом пакете она тоже есть и это его преимущество. Вверху иерархии находиться Connectors
там определяется базовая логика и данные. Например, что там может быть:
- базовый url куда отправлять запросы
- заголовки по умолчанию
- timeouts
- Заголовки для аутентификации или используя сертификат
- собственно все что будет использоваться в каждом запросе.
<?php
use Saloon\Http\Connector;
class ForgeConnector extends Connector
{
// попытки отправлять
public ?int $tries = 3;
// интервал в ms через который пытаться
public ?int $retryInterval = 500;
public function __construct(public readonly string $token) {}
public function resolveBaseUrl(): string
{
return 'https://forge.laravel.com/api/v1'; // можно взять через .env
}
protected function defaultHeaders(): array
{
return [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
// добавляем заголовки для аутентификации
protected function defaultAuth(): BasicAuthenticator
{
return new BasicAuthenticator($this->username, $this->password);
}
// можно написать свою стратегию когда надо делать retry а когда нет
// еще и перед retry переключиться на fallback решение или что-то
// в заголовки добавить
public function handleRetry(
FatalRequestException|RequestException $exception,
Request $request,
): bool
{
if ($exception instanceof RequestException &&
$exception->getResponse()->status() === 401
){
$request->authenticate(
new TokenAuthenticator($this->getNewToken())
);
}
return true;
}
}
Далее – частью этой структуры в коде являются Requests
– каждый класс это отдельный запрос, со своим path
в url, GET аргументами в query url или в body если например у вас POST запрос.
<?php
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetServersRequest extends Request
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return '/servers';
}
// массив сам подставиться в query url
protected function defaultQuery(): array
{
return [
'sort' => 'name', // ?sort=name
'filter[active]' => 'true', // ?filter[active]=true
];
}
}
В дополнении, можно выбрать формат отправки в body XML or JSON а может что еще – я на текущий момент кроме JSON ничего и не использовал.
Response
Обработка ответа после запроса это отдельная тема. В пакете есть отдельный класс с различными методами. Там в целом ничего нового. НО:
- Во-первых есть возможность определить свой custom response класс – с отдельными методами для вашей интеграции, а не только эти скучные
->isOk()
->json()
и так далее. - А во вторых, что мне нравиться еще больше, у вас есть возможность вернуть свой Dto класс. Делается это просто – пишите структуру Dto – и просто создаете его. Далее в клиентском коде он “автоматически возвращается”. Вот пример из документации
<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class GetServerRequest extends Request
{
// {...}
// метод который определяет создания Dto
public function createDtoFromResponse(Response $response): Server
{
$data = $response->json();
return new Server(
id: $data['id'],
name: $data['name'],
ipAddress: $data['ip'],
);
}
}
А вот пример вызовы в клиентском коде
<?php
$connector = new ForgeConnector;
$response = $connector->send(new GetServerRequest(id: 12345));
// Возращаем свое dto которые мы сконфигурировали выше
/** @var Server $server */
$server = $response->dto();
Тут конечно нюанс в виде комментария(аннотации) сверху. Так как type из метода “универсальный” – вам надо будет добавлять такие комментарии – чтобы ide понимала о каком типе идет речь.
Ну а далее все прекрасно – вы в этом Dto пишите свои методы которые красиво и изящно вам выдают информацию в вашей бизнес модели.
Exceptions и обработка ошибок
Об этом немало важном нюансе тоже подумали создатели библиотеки. Обработка ошибок есть в двух видах:
- Через exceptions
Внутри уже есть свой список exceptions на кода от 400 до 500 – но вы может и определить свой через метод
getRequestException
. Что еще мне понравилось, это отдельный метод для определения – был ли запрос failed или нет. Как в отдельном Request классе, так и в Connector по усмотрению.
<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class ErrorRequest extends Request
{
// {...}
public function hasRequestFailed(Response $response): ?bool
{
return str_contains($response->body(), 'Server Error');
}
}
Все мы знаем какие бывают API и иногда полезно пометить запрос Failed – даже если статус 200. 😁
- И конечно-ж через старый добрый if else. Сами определили статус ответа, его тела и сделали соответствующие выводы.
Пример 👇
<?php
$forge = new ForgeConnector;
$response = $forge->send(new ErrorRequest);
$response->failed(); // true
$response->status(); // 500
$response->body(); // {"message": "Server Error"}
Logging
Мониторинг важная часть и пару копеек о ней в этом пакете.
Сам автор в документации предлагает логировать через дополнительный функционал плагинов. Вы создаете trait а потом его добавляете или в глобальный connector
или в класс конкретного request
. Далее уже магия за кулисами автоматически вызовет методы у которых prefix начинается c boot. (где-то я уже видел??)
Второй вариант это использовать middleware – знакомая большинству конструкция – которая может перехватывать вызовы до и после. На моем личном опыте я не использовал оба варианта. В дополнение к вышеперечисленным способам – есть еще plugin для интеграции с laravel. Я “подцепился” на события при отправке и получении запроса и далее уже писал в лог информацию.
Про тестирование...
Документация здесь
Если вы ранее работали с тестами http client в Laravel – тот тут похожая концепция. Мы можем делать “моки” для ответов по url(так же sequence – проще говоря написать несколько ответов). Так и проверять что мы отправили в requests.
Пример 👇
use Saloon\Http\Faking\MockClient;
test('my test', function () {
$mockClient = new MockClient([
GetServersRequest::class => MockResponse::make(body: '', status: 200),
]);
$connector = new ForgeConnector;
$connector->withMockClient($mockClient);
]);
Все выглядит очень похоже как в laravel
Дополнение если вы еще почти хотите попробывать - но еще сомневаетесь...
Помимо ключевых возможностей есть еще множество дополнительных функций которые будут полезны:
- Delay – можно определить задержки в запросах
- Паралелльные запросы Concurrency requests
- Поддержка Auth 2.0
я не пользовался но из документации выглядить неплохо
- Поддержка PSR
Мой скромный вывод
В целом мне библиотека понравилась своей готовой структурой и экосистемой для различных случаев. Конечно может чего-то не хватать – но ее можно расширить за счет различных механизмов типа middleware, plugins – в добавок есть интеграция с Laravel. Учитываете что вам придется использовать Pest для тестирования (особо ничего сложного – просто нюанс).
Еще мне не понравилась работа в клиентском коде. Вам придется создавать для каждого запроса (может есть альтернативы и я плохо смотрел, если нашли напишите в комментариях!) объект и писать следующую конструкцию.
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$response = $forge->send($request);
$status = $response->status();
$body = $response->body();
Безусловно мы можем прокидывать ForgeConnector
через container и, но создавать каждый request объект не выглядит как код SDK client моей мечты. Поэтому вы можете создать обертку над всем этим и прокинуть только тот метод, который будет явно указывать что делать и какие аргументы передать. Например, так
<?php
//.. получили $client из container
// а внутри уже все что выше инкапсулированно
$client->getServers();
<?php
class Client {
// реализация в вашей обертке ...
public function getServers(): Response {
$request = new GetServersRequest;
return $forge->send($request);
}
}