Мой Telegram канал 👉 https://t.me/agoalofalife_channel
В жизни каждого программиста возникает тот момент когда необходимо выбрать рекурсию или цикл. Почти с большей вероятностью в жизни каждого разработчика стоит выбор между rm
или rm -f
в консоли. Но! Без сомнений для каждого web-разработчика был опыт в взаимодействия по API с внешними системами или с другой командой. Проще говоря, у вас есть API и вам надо написать код – который будет принимать данные и отправлять их.
Так вот … у каждого web ремесленника, спустя годы может выработаться свое видение как реализовывать такие вещи и сегодня я хочу добавить в общий котел инструментов – библиотеку для интеграции с API и дать краткое представление об этой “альтернативе”.
Барабанная дробь… Тишина … кулисы открылись и вот он: Saloon !
Пакет, который поможет вам сделать свой SDK или интеграцию с внешней системой. Что собственного в нем такого интересного – рассмотрим с “высоты птичьего полета”.
Под капотом у библиотеки используется Guzzle, что не удивительно.
Первое что стоит выделить – это структура всей “этой истории”. Структура врочем важна как и во многих вещах в коде.(и не только в коде кстати 😊).
Например, в вашем любимом фреймворке есть структура(папки, названия классов и т.д) – если вы им пользуетесь то другой разработчик ожидает ее увидеть(так конечно не всегда – но часто должно быть эх…). В этом пакете она тоже есть и это его преимущество. Вверху иерархии находиться Connectors
там определяется базовая логика и данные. Например, что там может быть:
<?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 ничего и не использовал.
Обработка ответа после запроса это отдельная тема. В пакете есть отдельный класс с различными методами. Там в целом ничего нового. НО:
->isOk()
->json()
и так далее.<?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 пишите свои методы которые красиво и изящно вам выдают информацию в вашей бизнес модели.
Об этом немало важном нюансе тоже подумали создатели библиотеки. Обработка ошибок есть в двух видах:
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. 😁
Пример 👇
<?php
$forge = new ForgeConnector;
$response = $forge->send(new ErrorRequest);
$response->failed(); // true
$response->status(); // 500
$response->body(); // {"message": "Server Error"}
Мониторинг важная часть и пару копеек о ней в этом пакете.
Сам автор в документации предлагает логировать через дополнительный функционал плагинов. Вы создаете 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
Не так давно в Laravel был смержен PR на добавления нового Facade с названием Context
.
Для чего он и что он дает пользователям фреймворка бы попробуем разобрать в этой статье.
По заявлению автора этого фасада, он позволит отслеживать текущий и исторический “контекст” приложения сквозь request, очереди и команды. Со слов автора Context
больше всего будет полезен для логгирования.
По сути основные точки входа в приложение, CLI (командная строка), HTTP и jobs.
Пока еще слабо понятно о чем идет речь, но примеры с кодом всегда нам дадут больше чем просто слова.
Talk is cheap. Show me the code.
В данном примере мы добавляем в context метаданные.
trace_id
используется в различных системах чтобы отследить flow или жизнь пользователя в приложении. Например вы можете передаватьtrace_id
в другой сервис(если он ваш) гдеtrace_id
будет тоже логироваться. В результате вы можете по логам восстановить “жизненный цикл” пользователя из различных сис-м по этой метке.
// где-то в middleware...
Context::add('hostname', gethostname());
Context::add('trace_id', (string) Str::uuid());
// где-то в controller...
Log::info('Retrieving commit messages for repository [{repository}].', [
'repository' => $repo,
]);
Http::get('https://laravel.su');
Теперь это информация добавлена в лог в качестве мета данных. 👇
[2024-01-19 04:20:06] production.INFO: Retrieving commit messages for repository [laravel/framework]. {"repository":"laravel/framework"} {"hostname":"prod-web-1","trace_id":"a158c456-d277-4214-badd-0f4c8e84df79"}
Фасад Context
может проносить содержимое через request запрос до очередей.
Например мы добавили в контекст информацию.
// Request::url() === 'https://laravel.su'
Context::add('initiating_url', Request::url());
Далее вы отправляете job в очереди как обычно.
CalculateStats::dispatch();
Теперь когда отложенная job выполниться, она имеет доступ к контексту из запроса.
class CalculateStats
{
public function handle()
{
Log::info('Hello from the queue.');
}
}
И результат в логе.
[2024-01-19 04:20:06] production.INFO: Hello from the queue. {"initiating_url":"https://laravel.su"}
Данные которые мы добавили во время запроса, будут доступны в очередях – даже если он выполниться на другой машине!
Как я писал выше, иногда необходимо передать trace_id
в другой сервис для связывания логической цепочки запросов в разных местах. Например в middleware мы может добавить этот trace_id
а потом его просто извлекать и вставлять в http запрос.
Context::add('trace_id', (string) Str::uuid());
// ...
Http::globalOptions([
'headers' => [
'X-Trace-Id' => Context::get('trace_id'),
],
]);
Более подробно вы можете ознакомиться с возможностями в этом PR
Телеграм канал где у нас свой контекст и своя атмосфера 😅
{message}