• Час читання ~13 хв
  • 15.06.2022

Ми всі були там, ми хочемо інтегруватися зі стороннім API в Laravel, і ми запитуємо себе: «Як мені це зробити?». Коли справа доходить до інтеграції API, я не знайомий, але щоразу я думаю, що буде найкращим способом. Сем Карре створив пакет на початку 2022 року під назвою Saloon, який може зробити нашу інтеграцію API дивовижною. Однак ця стаття буде зовсім іншою, це буде пояснення того, як ви можете використовувати її для створення інтеграції з нуля.

Як і всі чудові речі, він починається з laravel new і продовжується звідти, тож почнемо.Тепер, коли справа доходить до встановлення Laravel, ви можете використовувати інсталятор laravel або composer - це вирішувати вам. Я б рекомендував інсталятор, якщо ви можете, оскільки він надає прості варіанти, щоб зробити більше, ніж просто створити проект. Створіть новий проект і відкрийте його у вибраному редакторі коду. Коли ми будемо там, ми зможемо почати.

Що ми будемо будувати? Я радий, що ти запитав!Ми збираємося створити інтеграцію з API GitHub, щоб отримати список робочих процесів, доступних для репозиторії. Тепер це може бути дуже корисно, якщо ви, як і я, проводите багато часу в командному рядку. Ви працюєте над програмою, надсилаєте зміни до гілки або створюєте PR – це відбувається через робочий процес, який може виконувати одну з багатьох інших речей.Знання статусу цього робочого процесу іноді має величезний вплив на те, що ви робите далі. Ця функція завершена? Чи були проблеми з нашим робочим процесом? Чи проходять наші тести чи статичний аналіз? Усі ці речі ви зазвичай чекали і перевіряли репо на GitHub, щоб побачити статус.Ця інтеграція дозволить вам запустити команду ремісника, отримати список доступних робочих процесів для репозиторії та дозволити вам запустити новий робочий процес.

Отже, composer вже мав би зробити свою справу та встановити ідеальну відправну точку, програму Laravel.Далі нам потрібно встановити Saloon, але ми хочемо переконатися, що ми інсталюємо версію laravel, тому запустіть у своєму терміналі наступне:

composer require sammyjo20/saloon-laravel

Таким чином, ми вже на крок ближче до легшої інтеграції. Якщо на цьому етапі у вас виникли проблеми, переконайтеся, що ви перевірили версії Laravel і PHP, які ви використовуєте, оскільки Saloon вимагає принаймні Laravel 8 і PHP 8!

Отже, тепер ми встановили Saloon, нам потрібно створити новий клас. У термінології Saloons це «коннектори», і все, що робить з’єднувач, – це створює об’єктний спосіб сказати – цей API під’єднаний через цей клас.Існує зручна команда artisan, яка дозволяє створити їх, тому виконайте таку команду artisan, щоб створити з’єднувач GitHub:

php artisan saloon:connector GitHub GitHubConnector

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

Це створить для вас новий клас у app/Http/Integrations/GitHub/GitHubConnector.php, давайте подивимося на це на мить і зрозуміємо, що відбувається.

Перше, що ми бачимо, це те, що наш з’єднувач розширює SaloonConnector, що дозволить нам змусити наш з’єднувач працювати без великої кількості шаблонного коду. Потім ми успадковуємо ознаку під назвою AcceptsJson. Тепер, якщо ми подивимося на документацію Saloon, ми знаємо, що це плагін.Це в основному додає заголовок до наших запитів, який повідомляє сторонньому API, що ми хочемо приймати відповіді JSON.Наступне, що ми бачимо, це те, що у нас є метод визначення базової URL-адреси для нашого конектора, тому давайте додамо наш:

public function defineBaseUrl(): string
{
    return 'https://api.github.com';
}

Гарно та чисто, ми могли б навіть трохи розширити це питання, щоб мати справу з менш вільними струнами, які висять у нашій програмі, тож давайте подивимося, як це зробити. Всередині вашого config/services.php файл додати новий службовий запис:

'github' => [
    'url' => env('GITHUB_API_URL', 'https://api.github.com'),
]

Це дозволить нам замінити це в різних середовищах, що дасть нам краще та більш перевірене рішення. На локальному рівні ми могли б навіть висміяти GitHub API, використовуючи їх специфікацію OpenAPI, і перевірити, чи він працює. Однак цей посібник стосується салону, тому я відволікаюся...Тепер давайте переробимо наш метод базової URL-адреси, щоб використати конфігурацію:

public function defineBaseUrl(): string
{
    return (string) config('services.github.url');
}

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

Далі ми маємо заголовки за замовчуванням і конфігурацію за замовчуванням, зараз я не збираюся турбуватися про заголовки за замовчуванням, оскільки через деякий час ми підходимо до аутентифікації самостійно. Але в конфігурації ми можемо визначити параметри guzzle для нашої інтеграції, оскільки Saloon використовує Guzzle під капотом.Наразі давайте встановимо час очікування та рухаємося далі, але не соромтеся витратити деякий час на налаштування цього, як вважаєте за потрібне:

public function defaultConfig(): array
{
    return [
        'timeout' => 30,
    ];
}

Тепер наш конектор налаштовано так, як нам потрібно зараз, ми можемо повернутися пізніше, якщо знайдемо щось, що нам потрібно додати. Наступний крок — подумати про запити, які ми хочемо надіслати.Якщо ми подивимося на документацію API для GitHub Actions API, у нас є багато варіантів, ми почнемо з переліку робочих процесів для певного сховища: /repos/{owner}/{repo}/actions/workflows. Виконайте таку команду ремісника, щоб створити новий запит:

php artisan saloon:request GitHub ListRepositoryWorkflowsRequest

Знову перший аргумент – це інтеграція, а другий аргумент – ім’я запиту, який ми хочемо створити.Нам потрібно переконатися, що ми назвали інтеграцію для запиту, який ми створюємо, щоб він знаходився в потрібному місці, потім нам потрібно дати йому ім’я. Я назвав свій ListRepositoryWorkflowsRequest, тому що мені подобається описовий підхід до іменування, але не соромтеся адаптувати його до того, як вам подобається називати речі, оскільки тут немає справжнього неправильного способу.Це створить новий файл, який ми зможемо переглянути: app/Http/Integrations/GitHub/Requests/ListRepositoryWorkflowsRequest.php - давайте подивимося на це зараз.

Знову ми розширюємо тут бібліотечний клас, цього разу SaloonRequest, якого слід очікувати. Тоді ми маємо властивість конектора та метод.Ми можемо змінити метод, якщо нам потрібно, але стандартний GET – це те, що нам зараз потрібно. Тоді ми маємо метод визначення кінцевої точки. Переформуйте ваш клас запиту, щоб він виглядав як наведений нижче приклад:

class ListRepositoryWorkflowsRequest extends SaloonRequest
{
    protected ?string $connector = GitHubConnector::class;
 
    protected ?string $method = Saloon::GET;
 
    public function __construct(
        public string $owner,
        public string $repo,
    ) {}
 
    public function defineEndpoint(): string
    {
        return "/repos/{$this->owner}/{$this->repo}/actions/workflows";
    }
}

Ми додали конструктор, який приймає репо та власника як аргументи, які ми потім можемо використовувати в нашому методі кінцевої точки define.Ми також встановили конектор на GitHubConnector, який ми створили earler. Тож у нас є запит, який ми знаємо, що ми можемо надіслати, ми можемо зробити невеликий крок від інтеграції та замість цього подумати про консольну команду.

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

php artisan make:command GitHub/ListRepositoryWorkflows

Це створить такий файл: app/Console/Commands/GitHub/ListRespositoryWorkflows.php. Тепер ми можемо почати працювати з нашою командою, щоб надіслати запит і отримати потрібні нам дані.

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

Тепер ми можемо розширити нашу службу в config/services.php під github, щоб додати цей маркер:

GITHUB_API_TOKEN=ghp_loads-of-letters-and-numbers-here

Тепер у нас є хороший спосіб завантажити цей маркер, ми можемо повернутися до нашої консольної команди!Нам потрібно змінити наш підпис, щоб дозволити прийняти власника та репозиторій як аргументи:

'github' => [
    'url' => env('GITHUB_API_URL', 'https://api.github.com'),
    'token' => env('GITHUB_API_TOKEN'),
]

Тепер ми можемо зосередитися на методі дескриптора:

class ListRepositoryWorkflows extends Command
{
    protected $signature = 'github:workflows
        {owner : The owner or organisation.}
		{repo : The repository we are looking at.}
	';
 
    protected $description = 'Fetch a list of workflows from GitHub by the repository name.';
 
    public function handle(): int
    {
        return 0;
    }
}

Тут ми починаємо створювати наш запит, передаючи аргументи безпосередньо в сам запит, однак ми можемо захотіти створити деякі локальні змінні, щоб надати деякий зворотний зв’язок консолі:

public function handle(): int
{
    $request = new ListRepositoryWorkflowsRequest(
        owner: $this->argument('owner'),
        repo: $this->argument('repo'),
    );
 
    return self::SUCCESS;
}

Тож у нас є відгукикористувача, що завжди важливо, коли справа доходить до консольної команди. Тепер нам потрібно додати наш маркер аутентифікації та фактично надіслати запит:

public function handle(): int
{
    $owner = (string) $this->argument('owner');
    $repo = (string) $this->argument('repo');
 
    $request = new ListRepositoryWorkflowsRequest(
        owner: $owner,
        repo: $repo,
    );
 
    $this->info(
        string: "Fetching workflows for {$owner}/{$repo}",
    );
 
    return self::SUCCESS;
}

Якщо ви внесете поправки до вищезазначеного та виконаєте dd() на $response->json(), поки що. Потім запустіть команду:

public function handle(): int
{
    $owner = (string) $this->argument('owner');
    $repo = (string) $this->argument('repo');
 
    $request = new ListRepositoryWorkflowsRequest(
        owner: $owner,
        repo: $repo,
    );
 
    $request->withTokenAuth(
        token: (string) config('services.github.token'),
    );
 
    $this->info(
        string: "Fetching workflows for {$owner}/{$repo}",
    );
 
    $response = $request->send();
 
    return self::SUCCESS;
}

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

php artisan github:workflows laravel laravel

Тепер відповідь, яку ми отримуємо від API GitHub, чудова та інформативна, але для відображення її потрібно буде трансформувати, і якщо ми подивимося на неї ізольовано, контексту немає. Замість цього ми додамо до нашого запиту інший плагін, який дозволить нам трансформувати відповіді в DTO (об’єкти передачі домену), що є чудовим способом впоратися з цим.Це дозволить нам позбутися гнучкого масиву, який ми звикли отримувати від API, і отримати щось, що більш контекстуально обізнане. Давайте створимо DTO для робочого процесу, створимо новий файл: app/Http/Integrations/GitHub/DataObjects/Workflow.php і додайте до нього наступний код:

У нас є конструктор, який містить важливі частини нашого робочого процесу, які ми хочемо відобразити, метод fromSaloon, який перетворює масив із відповіді saloon на новий DTO, і метод to array для відображення DTO назад у масив, коли нам це потрібно.Всередині нашого ListRepositoryWorkflowsRequest нам потрібно успадкувати нову ознаку та додати новий метод:

class Workflow
{
    public function __construct(
        public int $id,
        public string $name,
        public string $state,
    ) {}
 
    public static function fromSaloon(array $workflow): static
    {
        return new static(
            id: intval(data_get($workflow, 'id')),
            name: strval(data_get($workflow, 'name')),
            state: strval(data_get($workflow, 'state')),
        );
    }
 
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'state' => $this->state,
        ];
    }
}

Ми успадковуємо ознаку CastsToDto, яка дозволяє цьому запиту викликати метод dto у відповіді, а потім ми додаємо метод castToDto де ми можемо контролювати, як це трансформується.Ми хочемо, щоб це повернуло нову колекцію, оскільки існує більше одного робочого процесу, використовуючи частину робочих процесів тіла відповіді. Потім ми відображаємо кожен елемент колекції - і перетворюємо його на DTO. Тепер ми можемо зробити це або так, або ми можемо зробити це так, коли ми створюємо нашу колекцію за допомогою DTO:

class ListRepositoryWorkflowsRequest extends SaloonRequest
{
    use CastsToDto;
 
    protected ?string $connector = GitHubConnector::class;
 
    protected ?string $method = Saloon::GET;
 
    public function __construct(
        public string $owner,
        public string $repo,
    ) {}
 
    public function defineEndpoint(): string
    {
        return "/repos/{$this->owner}/{$this->repo}/actions/workflows";
    }
 
    protected function castToDto(SaloonResponse $response): Collection
    {
        return (new Collection(
            items: $response->json('workflows'),
        ))->map(function ($workflow): Workflow {
            return Workflow::fromSaloon(
                workflow: $workflow,
            );
        });
    }
}

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

protected function castToDto(SaloonResponse $response): Collection
{
	return new Collection(
		items: $response->collect('workflows')->map(fn ($workflow) =>
			Workflow::fromSaloon(
				workflow: $workflow
			),
		)
	);
}

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

public function handle(): int
{
    $owner = (string) $this->argument('owner');
    $repo = (string) $this->argument('repo');
 
    $request = new ListRepositoryWorkflowsRequest(
        owner: $owner,
        repo: $repo,
    );
 
    $request->withTokenAuth(
        token: (string) config('services.github.token'),
    );
 
    $this->info(
        string: "Fetching workflows for {$owner}/{$repo}",
    );
 
    $response = $request->send();
 
    if ($response->failed()) {
        throw $response->toException();
	}
 
    $this->table(
        headers: ['ID', 'Name', 'State'],
        rows: $response
			->dto()
			->map(fn (Workflow $workflow) =>
				  $workflow->toArray()
			)->toArray(),
    );
 
    return self::SUCCESS;
}

Нарешті, просто перерахувати ці робочі процеси – це чудово, але давайте зробимо ще один крок вперед в ім’я науки. Скажімо, ви виконували цю команду для одного зі своїх репозиторій і хотіли запустити журнал змін вручну?Або, можливо, ви хотіли, щоб це запускалося на cron за допомогою вашого живого виробничого сервера або будь-якої події, про яку ви думаєте? Ми могли б налаштувати, щоб журнал змін запускався один раз на день опівночі, щоб ми отримували щоденні підсумки в журналі змін або все, що нам забажалося.Давайте створимо іншу консольну команду, щоб створити нову подію відправлення робочого процесу:

php artisan github:workflows laravel laravel
Fetching workflows for laravel/laravel
+----------+------------------+--------+
| ID       | Name             | State  |
+----------+------------------+--------+
| 12345678 | pull requests    | active |
| 87654321 | Tests            | active |
| 18273645 | update changelog | active |
+----------+------------------+--------+

Усередині цього нового файлу app/Http/Integrations/GitHub/Requests/CreateWorkflowDispatchEventRequest.php додайте такий код, щоб ми могли пройти через нього:

php artisan saloon:request GitHub CreateWorkflowDispatchEventRequest

Ми встановлюємо конектор і успадковуємо ознаку HasJsonBody, щоб дозволити нам надсилати дані.Метод був встановлений як запит POST, оскільки ми хочемо надіслати дані. Тоді у нас є конструктор, який приймає частини URL-адреси, яка створює кінцеву точку. Нарешті ми маємо дані купола за замовчуванням всередині defaultData, які ми можемо використовувати для встановлення значень за замовчуванням для цього запиту публікації.

class CreateWorkflowDispatchEventRequest extends SaloonRequest
{
    use HasJsonBody;
 
    protected ?string $connector = GitHubConnector::class;
 
    public function defaultData(): array
    {
        return [
            'ref' => 'main',
        ];
    }
 
    protected ?string $method = Saloon::POST;
 
    public function __construct(
        public string $owner,
        public string $repo,
        public string $workflow,
    ) {}
 
    public function defineEndpoint(): string
    {
        return "/repos/{$this->owner}/{$this->repo}/actions/workflows/{$this->workflow}/dispatches";
    }
}

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

php artisan make:command GitHub/CreateWorkflowDispatchEvent
class CreateWorkflowDispatchEvent extends Command
{
    protected $signature = 'github:dispatch
        {owner : The owner or organisation.}
		{repo : The repository we are looking at.}
		{workflow : The ID of the workflow we want to dispatch.}
		{branch? : Optional: The branch name to run the workflow against.}
	';
 
    protected $description = 'Create a new workflow dispatch event for a repository.';
 
    public function handle(): int
    {
        $owner = (string) $this->argument('owner');
        $repo = (string) $this->argument('repo');
        $workflow = (string) $this->argument('workflow');
 
        $request = new CreateWorkflowDispatchEventRequest(
            owner: $owner,
            repo: $repo,
            workflow: $workflow,
        );
 
        $request->withTokenAuth(
            token: (string) config('services.github.token'),
        );
 
		if ($this->hasArgument('branch')) {
			$request->setData(
				data: ['ref' => $this->argument('branch')],
			);
        }
 
        $this->info(
            string: "Requesting a new workflow dispatch for {$owner}/{$repo} using workflow: {$workflow}",
        );
 
        $response = $request->send();
 
        if ($response->failed()) {
            throw $response->toException();
        }
 
        $this->info(
            string: 'Request was accepted by GitHub',
        );
 
        return self::SUCCESS;
    }
}
on: workflow_dispatch

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