• Время чтения ~13 мин
  • 15.06.2022

Все мы сталкивались с этим, мы хотим интегрироваться со сторонним API в Laravel и спрашиваем себя: "Как мне это сделать?". Когда дело доходит до интеграции API, я не новичок, но все же каждый раз я думаю, что будет лучше всего. Сэм Карре в начале 2022 года создал пакет под названием Saloon, который может сделать нашу интеграцию API потрясающей. Эта статья, однако, будет совсем другой, это будет пошаговое руководство о том, как вы можете использовать ее для создания интеграции с нуля.

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

Что мы собираемся строить? Я рад, что вы спросили!Мы собираемся создать интеграцию с GitHub API, чтобы получить список рабочих процессов, доступных для репозитория. Теперь это может быть очень полезно, если вы, как и я, проводите много времени в командной строке. Вы работаете над приложением, отправляете изменения в ветку или создаете 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

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

Это создаст для вас новый класс в 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, и протестировать ее, чтобы убедиться, что она работает. Тем не менее, это руководство посвящено Saloon, поэтому я отвлекся...Теперь давайте реорганизуем наш базовый метод URL, чтобы использовать конфигурацию:

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

Как видите, теперь мы извлекаем только что добавленную запись из нашей конфигурации — и преобразуем ее в строку для безопасности типов — config() возвращает смешанный результат, поэтому мы хотим быть строгими это если сможем.

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

Что мы сделали, так это добавили конструктор, который принимает репозиторий и владельца в качестве аргументов, которые мы затем можем использовать в нашем методе определения конечной точки.Мы также установили коннектор на GitHubConnector, который создали ранее. Итак, у нас есть запрос, который мы можем отправить, мы можем сделать небольшой шаг в сторону от интеграции и вместо этого подумать о консольной команде.

Если вы раньше не создавали консольную команду в 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'),
]

Теперь мы можем сосредоточиться на методе handle:

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

Теперь ответ, который мы получаем от GitHub API, отличный и информативный, но он потребует преобразования для отображения, и если мы посмотрим на него изолированно, контекста не будет. Вместо этого мы добавим в наш запрос еще один плагин, который позволит нам преобразовывать ответы в DTO (объекты передачи домена), что является отличным способом справиться с этим.Это позволит нам отказаться от гибкого массива, который мы привыкли получать от API, и получить что-то более контекстно-зависимое. Давайте создадим DTO для рабочего процесса, создадим новый файл: app/Http/Integrations/GitHub/DataObjects/Workflow.php и добавьте в него следующий код:

У нас есть конструктор, содержащий важные части нашего рабочего процесса, которые мы хотим отобразить, метод fromSaloon, который преобразует массив из ответа салона в новый DTO, и метод массива для отображения 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