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

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

Тому я працюю зі сторонніми сервісами так довго, що не пам'ятаю, коли не був. Як молодший розробник, я інтегрував API в інші платформи, такі як Joomla, Magento та WordPress. Тепер він в основному інтегрується в мої програми Laravel, щоб розширити бізнес-логіку, спираючись на інші послуги.

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

Почнемо з API. Нам потрібен API для інтеграції. Моїм оригінальним підручником була інтеграція з PingPing, відмінним рішенням для моніторингу часу роботи від спільноти Laravel. Однак цього разу я хочу спробувати інший API.

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

Що дасть наша інтеграція? Уявіть, що у нас є програма, яка дозволяє нам керувати нашою інфраструктурою. Наші сервери працюють через Laravel Forge, а наша база даних закінчилася на Planetscale. Чистого способу управління цим робочим процесом немає, тому ми створили свій. Для цього нам потрібна інтеграція-дві.

Спочатку я зберігав свої інтеграції під app/Services; однак, оскільки мої програми стали більш широкими та складними, мені потрібно було використовувати Services простір імен для внутрішніх служб, що призвело до забрудненого простору імен. Я перемістив свої інтеграції до app/Http/Integrations. Це має сенс, і це трюк, який я взяв із салону Сема Карре.

Тепер я міг використовувати Saloon для інтеграції API, але я хотів пояснити, як я це роблю без пакета. Якщо вам потрібна інтеграція API у 2023 році, я настійно рекомендую використовувати Saloon. Це надзвичайно дивно!

Отже, почнемо зі створення каталогу для нашої інтеграції. Ви можете використовувати таку команду bash:

mkdir app/Http/Integrations/Planetscale

Після того, як у нас є каталог Planetscale, нам потрібно створити спосіб підключення до нього. Ще одна угода про імена, яку я взяв з бібліотеки Saloon, полягає в тому, щоб розглядати ці базові класи як роз'єми - оскільки їх призначення полягає в тому, щоб дозволити вам підключитися до певного API або третьої сторони.

Створіть новий клас, який називається PlanetscaleConnector в app/Http/Integrations/Planetscale каталозі, і ми зможемо конкретизувати, що потрібно цьому класу, що буде дуже весело.

Тому ми повинні зареєструвати цей клас у нашому контейнері, щоб вирішити його або побудувати фасад навколо нього. Ми могли б зареєструвати це "довгим" шляхом у постачальника послуг - але мій останній підхід полягає в тому, щоб ці конектори зареєструвалися - свого роду ...

declare(strict_types=1);

namespace App\Http\Integrations\Planetscale;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

final readonly class PlanetscaleConnector
{
    public function __construct(
        private PendingRequest $request,
    ) {}
    public static function register(Application $app): void
    {
        $app->bind(
            abstract: PlanetscaleConnector::class,
            concrete: fn () => new PlanetscaleConnector(
                request: Http::baseUrl(
                    url: '',
                )->timeout(
                    seconds: 15,
                )->withHeaders(
                    headers: [],
                )->asJson()->acceptJson(),
            ),
        );
    }
}

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

Ви помітите, що наразі ми нічого не передаємо методам токена або базової URL-адреси в запиті. Давайте виправимо це далі. Ви можете отримати їх у своєму обліковому записі Planetscale.

Створіть у .env файлі наведені нижче записи.

PLANETSCALE_SERVICE_ID="your-service-id-goes-here"
PLANETSCALE_SERVICE_TOKEN="your-token-goes-here"
PLANETSCALE_URL="https://api.planetscale.com/v1"

Далі їх потрібно витягнути в конфігурацію програми. Усі вони належать config/services.php , оскільки саме тут зазвичай налаштовуються сторонні служби.

return [
    // the rest of your services config

    'planetscale' => [
        'id' => env('PLANETSCALE_SERVICE_ID'),
        'token' => env('PLANETSCALE_SERVICE_TOKEN'),
        'url' => env('PLANETSCALE_URL'),
    ],
];

Тепер ми можемо використовувати їх у нашому PlanetscaleConnector методі під реєстром.

declare(strict_types=1);

namespace App\Http\Integrations\Planetscale;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

final readonly class PlanetscaleConnector
{
    public function __construct(
        private PendingRequest $request,
    ) {}
    public static function register(Application $app): void
    {
        $app->bind(
            abstract: PlanetscaleConnector::class,
            concrete: fn () => new PlanetscaleConnector(
                request: Http::baseUrl(
                    url: config('services.planetscale.url'),
                )->timeout(
                    seconds: 15,
                )->withHeaders(
                    headers: [
                'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
            ],
                )->asJson()->acceptJson(),
            ),
        );
    }
}

Вам потрібно надіслати токени до Planetscale у такому форматі: service-id:service-token, тому ми не можемо використовувати метод за замовчуванням withToken , оскільки він не дозволяє нам налаштувати його так, як нам потрібно.

Тепер, коли ми створили базовий клас, ми можемо почати думати про ступінь нашої інтеграції. Ми повинні зробити це під час створення нашого службового токена, щоб додати правильні дозволи. У нашому додатку ми хочемо мати можливість зробити наступне:
Список баз даних.
Список регіонів бази даних.
Список резервних копій бази даних.
Створіть резервну копію бази даних.
Видалити резервну копію бази даних.

Отже, ми можемо розглянути їх групування за двома категоріями:
Баз даних.
Резервні копії.

Давайте додамо два нових методи до нашого роз'єму, щоб створити те, що нам потрібно:

declare(strict_types=1);

namespace App\Http\Integrations\Planetscale;

use App\Http\Integrations\Planetscale\Resources\BackupResource;
use App\Http\Integrations\Planetscale\Resources\DatabaseResource;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

final readonly class PlanetscaleConnector
{
    public function __construct(
        private PendingRequest $request,
    ) {}
    public function databases(): DatabaseResource
    {
        return new DatabaseResource(
            connector: $this,
        );
    }
    public function backups(): BackupResource
    {
        return new BackupResource(
            connector: $this,
        );
    }
    public static function register(Application $app): void
    {
        $app->bind(
            abstract: PlanetscaleConnector::class,
            concrete: fn () => new PlanetscaleConnector(
                request: Http::baseUrl(
                    url: config('services.planetscale.url'),
                )->timeout(
                    seconds: 15,
                )->withHeaders(
                    headers: [
                        'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
                    ],
                )->asJson()->acceptJson(),
            ),
        );
    }
}

Як бачите, ми створили два нових методи, databases і backups. Вони повернуть нові класи ресурсів, що проходять через конектор. Логіка тепер може бути реалізована в класах ресурсів, але ми повинні додати інший метод до нашого конектора пізніше.

<?php
declare(strict_types=1);

namespace App\Http\Integrations\Planetscale\Resources;

use App\Http\Integrations\Planetscale\PlanetscaleConnector;

final readonly class DatabaseResource
{
    public function __construct(
        private PlanetscaleConnector $connector,
    ) {}
    public function list()
    {
        //
    }
    public function regions()
    {
        //
    }
}

Це наші DatabaseResourceметоди, які ми хочемо реалізувати. Ви можете зробити те ж саме для BackupResource. Він буде виглядати чимось схожим.

Таким чином, результати можуть бути розділені на сторінки баз даних. Однак я не буду тут цим займатися - я б для цього сперся на салон, так як його реалізація для пагінованих результатів фантастична. У цьому прикладі ми не будемо турбуватися про нумерацію сторінок. Перш ніж ми заповнимо DatabaseResource, нам потрібно додати ще один метод, PlanetscaleConnector щоб гарно надсилати запити. Для цього я використовую свій пакет під назвою juststeveking/http-helpers, який має enum для всіх типових методів HTTP, які я використовую.

public function send(Method $method, string $uri, array $options = []): Response
{
    return $this->request->send(
        method: $method->value,
        url: $uri,
        options: $options,
    )->throw();
}

Тепер ми можемо повернутися до нашого DatabaseResource і почати заповнювати логіку методу списку.

declare(strict_types=1);

namespace App\Http\Integrations\Planetscale\Resources;

use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use Illuminate\Support\Collection;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;

final readonly class DatabaseResource
{
    public function __construct(
        private PlanetscaleConnector $connector,
    ) {}
    public function list(string $organization): Collection
    {
        try {
            $response = $this->connector->send(
                method: Method::GET,
                uri: "/organizations/{$organization}/databases"
            );
        } catch (Throwable $exception) {
            throw $exception;
        }
        return $response->collect('data');
    }
    public function regions()
    {
        //
    }
}

Наш метод списку приймає параметр organization , який проходить через організацію для списку баз даних. Потім ми використовуємо це, щоб надіслати запит на певну URL-адресу через конектор. Загортання цього в заяву про спробу лову дозволяє нам виявити потенційні винятки з методу надсилання роз'ємів. Нарешті, ми можемо повернути колекцію з методу для роботи з нею в нашому додатку.

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

Давайте швидко розглянемо більше, BackupResource ніж просто запит на отримання.

declare(strict_types=1);

namespace App\Http\Integrations\Planetscale\Resources;

use App\Http\Integrations\Planetscale\Entities\CreateBackup;
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;

final readonly class BackupResource
{
    public function __construct(
        private PlanetscaleConnector $connector,
    ) {}
    public function create(CreateBackup $entity): array
    {
        try {
            $response = $this->connector->send(
                method: Method::POST,
                uri: "/organizations/{$entity->organization}/databases/{$entity->database}/branches/{$entity->branch}",
                options: $entity->toRequestBody(),
            );
        } catch (Throwable $exception) {
            throw $exception;
        }
        return $response->json('data');
    }
}

Наш метод create приймає клас сутності, який я використовую для передачі даних через додаток, де це необхідно. Це корисно, коли URL-адреса потребує набору параметрів, і нам потрібно надіслати тіло запиту.

Я не розглядав тестування тут, але я написав підручник про те, як перевірити кінцеві точки JSON:API за допомогою PestPHP тут, який матиме подібні концепції для тестування такої інтеграції.

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

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