• Время чтения ~7 мин
  • 21.02.2023

Снова и снова я говорил об интеграции API, и каждый раз, когда я нахожу улучшение по сравнению с предыдущим, снова вступаю в схватку.

Недавно я был отмечен в твиттере о статье под названием SDKs, The Laravel Way. Это выделялось тем, что несколько отражало то, как я делаю интеграцию API, и вдохновило меня добавить свои собственные два цента.

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

При построении новой интеграции API я сначала организую переменные конфигурации и среды. Для меня они принадлежат файлу конфигурации служб, поскольку они являются сторонними службами, которые мы настраиваем. Конечно, вам не нужно этого делать, и на самом деле не имеет значения, где вы их храните, пока вы помните, где они находятся!

// config/services.php
return [
    // other services

    'github' => [
        'url' => env('GITHUB_URL'),
        'token' => env('GITHUB_TOKEN'),
        'timeout' => env('GITHUB_TIMEOUT', 15),
    ],
];

Главное, что я обязательно делаю, это храню URL-адрес, токен API и тайм-аут. Я храню URL-адрес, потому что ненавижу плавающие строки в классах или коде приложения. Это кажется неправильным, это не неправильно, конечно, но мы все знаем, что у меня есть твердое мнение...

Затем я создаю контракт/интерфейс - в зависимости от того, сколько интеграций подскажут мне, какой интерфейс мне может понадобиться. Мне нравится использовать интерфейс, поскольку он обеспечивает выполнение контракта в моем коде и заставляет меня думать, прежде чем изменять API. Я защищаю себя от прорывных изменений! Мой контракт очень минимален. Обычно у меня есть один метод и блок doc для свойства, которое я собираюсь добавить через конструктор.

/**
 * @property-read PendingRequest $request
 */
interface ClientContract
{
    public function send();
}

Я оставляю ответственность за отправку запроса клиенту. Этот метод будет переработан позже, но я подробно расскажу об этом позже. Этот интерфейс может жить там, где вам удобнее всего его хранить - я обычно храню его в разделе App\Services\Contracts, но не стесняйтесь использовать свое воображение.

Как только у меня есть примерный контракт, я начинаю строить саму реализацию. Мне нравится создавать новое пространство имен для каждой интеграции, которую я создаю. Он держит вещи сгруппированными и логичными.

final class Client implements ClientContract
{
    public function __construct(
        private readonly PendingRequest $request,
    ) {}
}

Я начал передавать настроенный PendingRequest в клиент API, так как он сохраняет вещи в чистоте и избегает ручной настройки. Мне нравится такой подход, и я удивляюсь, почему я не делал этого раньше!

Вы заметите, что мне все еще нужно следовать контракту, так как есть шаг, который мне нужно сделать заранее. Одна из моих любимых функций PHP 8.1 - Enums.

Я создаю метод Enum для приложений, я должен сделать пакет, так как он сохраняет вещи текучими - и опять же, никаких плавающих строк в моем приложении!

enum Method: string
{
    case GET = 'GET';
    case POST = 'POST';
    case PUT = 'PUT';
    case PATCH = 'PATCH';
    case DELETE = 'DELETE';
}

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

Я могу рефакторить свой контракт, чтобы включить в него то, как я хочу, чтобы это работало.

/**
 * @property-read PendingRequest $request
 */
interface ClientContract
{
    public function send(Method $method, string $url, array $options = []): Response;
}

Метод отправки моих клиентов должен знать используемый метод, URL-адрес, на который он отправляется, и любые необходимые параметры. Это почти то же самое, что и метод отправки PendingRequest - кроме использования Enum для метода, это сделано специально.

Тем не менее, я не добавляю это к своему клиенту, так как у меня может быть несколько клиентов, которые хотят отправлять запросы. Поэтому я создаю проблему/черту, которую я могу добавить к каждому клиенту, позволяя ему отправлять запросы.

/**
 * @mixin ClientContract
 */
trait SendsRequests
{
    public function send(Method $method, string $url, array $options = []): Response
    {
        return $this->request->throw()->send(
            method: $method->value,
            url: $url,
            options: $options,
        );
    }
}

Это стандартизирует способ отправки запросов API и заставляет их автоматически создавать исключения. Теперь я могу добавить это поведение к самому клиенту, сделав его чище и минималистичнее.

final class Client implements ClientContract
{
    use SendsRequests;

    public function __construct(
        private readonly PendingRequest $request,
    ) {}
}

Отсюда я начинаю определять ресурсы и то, за что я хочу, чтобы ресурс отвечал. На данный момент все дело в дизайне объекта. Для меня ресурс — это конечная точка, внешний ресурс, доступный через транспортный уровень HTTP. Ему не нужно намного больше, чем это.

Как обычно, я создаю контракт/интерфейс, которому я хочу, чтобы все мои ресурсы следовали, что означает, что у меня есть предсказуемый код.

/**
 * @property-read ClientContract $client
 */
interface ResourceContract
{
    public function client(): ClientContract;
}

Мы хотим, чтобы наши ресурсы могли получить доступ к самому клиенту с помощью метода getter. Мы также добавляем клиент в качестве свойства docblock.

Теперь мы можем создать наш первый ресурс. Мы сосредоточимся на issues, так как это довольно захватывающая конечная точка. Давайте начнем с создания класса и его расширения.

final class IssuesResource implements ResourceContract
{
    use CanAccessClient;
}

Я создал здесь новую черту/проблему под названием CanAccessClient, так как все наши ресурсы, независимо от API, захотят получить доступ к своему родительскому клиенту. Я также перевел конструктора к этой черте / проблеме - я случайно обнаружил эту работу и полюбил ее. Я все еще определяю, всегда ли я буду делать это или держать это там, но это держит мои ресурсы чистыми и сосредоточенными - поэтому я буду держать это на данный момент. Я хотел бы услышать ваши мысли по этому поводу!

/**
 * @mixin ResourceContract
 */
trait CanAccessClient
{
    public function __construct(
        private readonly ClientContract $client,
    ) {}
    public function client(): ClientContract
    {
        return $this->client;
    }
}

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

final class Client implements ClientContract
{
    use SendsRequests;

    public function __construct(
        private readonly PendingRequest $request,
    ) {}
    public function issues(): IssuesResource
    {
        return new IssuesResource(
            client: $this,
        );
    }
}

Это позволяет нам иметь хороший и чистый API $client->issues()-> поэтому мы не полагаемся на магические методы или проксирование чего-либо - он чист и обнаруживается для нашей IDE.

Первый запрос, который мы захотим отправить, - это перечисление всех проблем для аутентифицированного пользователя. Конечной точкой API для этого является https://api.github.com/issues, что довольно просто. Давайте теперь посмотрим на наши запросы и на то, как мы хотим их отправить. Да, как вы уже догадались, для этого нам снова понадобится контракт/интерфейс.

/**
 * @property-read ResourceContract $resource
 */
interface RequestContract
{
    public function resource(): ResourceContract;
}

Out request реализует проблему/черту, которая позволит ему вызвать ресурс и передать нужный запрос обратно клиенту.

/**
 * @mixin RequestContract
 */
trait HasResource
{
    public function __construct(
        private ResourceContract $resource,
    ) {}
    public function resource(): ResourceContract
    {
        return $this->resource;
    }
}

Наконец, мы можем начать думать о запросе, который мы хотим отправить! Есть много шаблонов, чтобы добраться до этой стадии. Однако в долгосрочной перспективе оно того стоит. Мы можем точно настроить и внести изменения в любой точке этой цепочки, не внося критических изменений.

final class ListIssuesRequest implements RequestContract
{
    use HasResource;

    public function __invoke(): Response
    {
        return $this->resource()->client()->send(
            method: Method::GET,
            url: 'issues',
        );
    }
}

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

$client->issues()->list();

Мы работаем с Ответом Освещения для доступа к данным с этого момента, поэтому у него есть все удобные методы, которые мы можем захотеть. Однако это только иногда идеально. Иногда мы хотим использовать что-то более пригодное для использования в качестве объекта в нашем приложении. Для этого нам нужно взглянуть на трансформацию ответных мер.

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

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297