• Час читання ~7 хв
  • 21.02.2023

Знову і знову я говорив про інтеграцію API, і кожен раз, коли я знаходжу покращення від останнього, знову в бійку.

Нещодавно мене позначили в Twitter про статтю під назвою SDKs, The Laravel Way. Це виділилося, оскільки дещо віддзеркалювало те, як я роблю інтеграцію API, і надихнуло мене додати власні два центи.

Для запису згадана мною стаття відмінна, і в прийнятому підході немає нічого поганого. Я відчуваю, що можу трохи розширити його завдяки своїм урокам, отриманим у розробці та інтеграції API. Я не буду безпосередньо копіювати тему, оскільки вона призначена для API, з яким я не знайомий. Однак я візьму концепції в API, з яким я знайомий, API GitHub, і вдаюся до деяких деталей.

Створюючи нову інтеграцію 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. Я захищаю себе від зриву змін! Хоча мій контракт дуже мінімальний. Зазвичай у мене є один метод і блок документів для властивості, яку я маю намір додати через конструктор.

/**
 * @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.

Тепер ми можемо створити свій перший ресурс. Ми зосередимося на проблемах, оскільки це досить захоплююча кінцева точка. Почнемо зі створення класу і розширення на ньому.

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;
}

Аут-запит реалізує проблему/рису, яка дозволить йому зателефонувати на ресурс і передати бажаний запит назад клієнту. Нарешті, ми

/**
 * @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();

Ми працюємо з Illuminate Response, щоб отримати доступ до даних з цього моменту, тому він має всі методи зручності, які ми можемо захотіти. Однак це лише іноді ідеально. Іноді ми хочемо використовувати щось більш корисне як об'єкт у нашому додатку. Для цього нам потрібно подивитися на трансформацію відповіді.

Я не збираюся занадто багато вдаватися сюди з трансформацією відповіді та того, що ви можете зробити, оскільки я думаю, що це зробило б чудовий підручник самостійно. Повідомте нам, якщо ви вважаєте цей підручник корисним або якщо у вас є пропозиції щодо покращення цього процесу.

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