Поддержите проект сделав пожертвование.

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

Дополнение если вы еще почти хотите попробывать - но еще сомневаетесь...

Помимо ключевых возможностей есть еще множество дополнительных функций которые будут полезны:

Мой скромный вывод

В целом мне библиотека понравилась своей готовой структурой и экосистемой для различных случаев. Конечно может чего-то не хватать – но ее можно расширить за счет различных механизмов типа 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);
 }
}
Илья Чубаров (Переводчик)

Мой Telegram канал 👉 https://t.me/agoalofalife_channel

2

Вакансии

Спонсоры

Помощь в разработке вашего проекта на Laravel

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

Присоединиться

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

Перейти

Подкасты c зажигательными эпизодами, которые заставят задуматься и приведут к новым перспективам.

Перейти