• Время чтения ~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 Сэма Карре.

Теперь я мог бы использовать 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 в следующем формате: , поэтому мы не можем использовать метод по умолчаниюwithToken, поскольку он не позволяет нам настроить его так, service-id:service-tokenкак нам нужно.

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

Итак, мы можем сгруппировать их в две категории:
Баз данных.
Резервные копии.

Давайте добавим два новых метода в наш коннектор, чтобы создать то, что нам нужно:

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. Это будет выглядеть несколько похоже.

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

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

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

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-адрес через соединитель. Обертывание этого оператора try-catch позволяет нам перехватывать потенциальные исключения из метода отправки соединителей. Наконец, мы можем вернуть коллекцию из метода, чтобы работать с ней в нашем приложении.

Мы можем более подробно остановиться на этом запросе, так как мы можем начать сопоставлять данные из массивов с чем-то более контекстуально полезным с помощью 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