• Час читання ~10 хв
  • 21.08.2022

Як це вказати? Програми CLI – це круто. Можливість відкрити термінал у будь-якому місці та просто виконати команду, щоб виконати роботу, яка може зайняти у вас набагато більше часу. Відкрийте браузер, перейдіть на потрібну сторінку, увійдіть і знайдіть те, що вам потрібно зробити, і дочекайтеся, поки сторінка завантажиться.... Ви отримуєте картинку.

За останні кілька років у командний термінал було вкладено багато інвестицій;від ZSH до автозавершення, від FIG до Warp - CLI є те, чого ми не можемо уникнути. Я створюю програми CLI, які допомагають мені ефективніше виконувати невеликі завдання або виконувати роботу за розкладом.

Коли я переглядаю в Інтернеті щось, що стосується Laravel, це завжди веб-програма, і це має сенс. Зрештою, Laravel — це фантастичний фреймворк для веб-додатків!Однак використання того, що нам подобається в Laravel, також доступне для програм CLI. Тепер ми можемо використовувати повну інсталяцію Laravel і запустити планувальник, щоб виконувати команди ремісників там, де нам потрібно, але це іноді надмірно. Якщо вам не потрібен веб-інтерфейс, вам не потрібен Laravel. Натомість поговоримо про Laravel Zero, ще одне дітище Нуно Мадуро.

Laravel Zero описує себе як «мікрофреймворк для консолі» додаток" - це досить точно. Це дозволяє створювати додатки CLI, використовуючи перевірену структуру, яка є меншою, ніж використання чогось на зразок Laravel.Він добре задокументований, надійний і активно підтримується, що робить його ідеальним вибором для будь-яких програм CLI, які ви, можливо, захочете створити.

У цьому підручнику я розповім про все дещо простий приклад використання Laravel Zero з надією, що він покаже вам, наскільки він може бути корисним. Ми створимо додаток CLI, який дозволить нам бачити проекти та завдання в моєму <обліковий запис href="https://todoist.com/">Todoist, щоб мені не потрібно було відкривати програму чи веб-переглядач.

< p>Щоб почати, нам потрібно перейти до веб-програми для Todoist і відкрити налаштування інтеграції, щоб отримати маркер API. Це нам знадобиться трохи пізніше. Наш перший крок — створити новий проект Laravel Zero, який ми зможемо використовувати.

composer create-project --prefer-dist laravel-zero/laravel-zero todoist

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

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

declare(strict_types=1);
 
namespace App\Contracts;
 
interface ConfigurationContract
{
    public function all(): array;
 
    public function clear(): ConfigurationContract;
 
    public function get(string $key, mixed $default = null): array|int|string|null;
 
    public function set(string $key, array|int|string $value): ConfigurationContract;
}

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

declare(strict_types=1);
 
namespace App\Repositories;
 
use App\Contracts\ConfigurationContract;
use App\Exceptions\CouldNotCreateDirectory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
 
final class LocalConfiguration implements ConfigurationContract
{
    public function __construct(
        protected readonly string $path,
    ) {}
 
    public function all(): array
    {
        if (! is_dir(dirname(path: $this->path))) {
            if (! mkdir(
                    directory: $concurrentDirectory = dirname(
                        path: $this->path,
                    ),
                    permissions: 0755,
                    recursive: true
                ) && !is_dir(filename: $concurrentDirectory)) {
                throw new CouldNotCreateDirectory(
                    message: "Directory [$concurrentDirectory] was not created",
                );
            }
        }
 
        if (file_exists(filename: $this->path)) {
            return json_decode(
                json: file_get_contents(
                    filename: $this->path,
                ),
                associative: true,
                depth: 512,
                flags: JSON_THROW_ON_ERROR,
            );
        }
 
        return [];
    }
 
    public function clear(): ConfigurationContract
    {
        File::delete(
            paths: $this->path,
        );
 
        return $this;
    }
 
    public function get(string $key, mixed $default = null): array|int|string|null
    {
        return Arr::get(
            array: $this->all(),
            key: $key,
            default: $default,
        );
    }
 
    public function set(string $key, array|int|string $value): ConfigurationContract
    {
        $config = $this->all();
 
        Arr::set(
            array: $config,
            key: $key,
            value: $value,
        );
 
        file_put_contents(
            filename: $this->path,
            data: json_encode(
                value: $config,
                flags: JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT,
            ),
        );
 
        return $this;
    }
}

Ми використовуємо деякі допоміжні методи в Laravel і деякі базові PHP, щоб отримати вміст і перевірити файли - потім читати та писати вміст, де потрібно. Завдяки цьому ми можемо керувати файлом будь-де в нашій локальній файловій системі.Наш наступний крок — прив’язати це до нашого контейнера, щоб ми могли встановити нашу поточну реалізацію та те, як ми хочемо вирішити цю проблему з контейнера.

declare(strict_types=1);
 
namespace App\Providers;
 
use App\Contracts\ConfigurationContract;
use App\Repositories\LocalConfiguration;
use Illuminate\Support\ServiceProvider;
 
final class AppServiceProvider extends ServiceProvider
{
    public array $bindings = [
        ConfigurationContract::class => LocalConfiguration::class,
    ];
 
    public function register(): void
    {
        $this->app->singleton(
            abstract: LocalConfiguration::class,
            concrete: function (): LocalConfiguration {
                $path = isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing'
                    ? base_path(path: 'tests')
                    : ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']);
 
                return new LocalConfiguration(
                    path: "$path/.todo/config.json",
                );
            },
        );
    }
}

Ми прив’язуємо наш договір до нашої реалізації за допомогою властивості bindings постачальників послуг тут. Потім у методі register ми встановлюємо, як ми хочемо, щоб наша реалізація була побудована. Тепер, коли ми вводимо <код>ConfigurationContract у команду, ми отримаємо екземпляр LocalConfiguration, який було розв’язано як єдиний елемент.

Перше, що що ми зараз хочемо зробити з програмою Laravel Zero, це дати їй назву, щоб ми могли викликати програму CLI, використовуючи назву, яка відповідає тому, що ми створюємо. Я буду називати своє "todo".

php application app:rename todo

Тепер ми можемо викликати нашу команду за допомогою php todo ... і почати створювати команди CLI, які ми захочемо використовувати. Перш ніж створювати команди, нам потрібно буде створити клас, який інтегрується з Todoist API. Знову ж таки, я створю для цього інтерфейс/контракт, якщо вирішу перейти від Todoist до іншого постачальника.

declare(strict_types=1);
 
namespace App\Contracts;
 
interface TodoContract
{
    public function projects(): ResourceContract;
 
    public function tasks(): ResourceContract;
}

У нас є два методи: проекти і завдання, який поверне клас ресурсу для роботи. І, як зазвичай, цей клас ресурсів потребує контракту. Контракт ресурсу використовуватиме контракт об’єкта даних, але замість того, щоб створити його, я використаю той, який я вбудував у свій пакет:

composer require juststeveking/laravel-data-object-tools

Тепер ми можемо створити ресурс Сам контракт:

declare(strict_types=1);
 
namespace App\Contracts;
 
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
interface ResourceContract
{
    public function list(): Collection;
 
    public function get(string $identifier): DataObjectContract;
 
    public function create(DataObjectContract $resource): DataObjectContract;
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract;
 
    public function delete(string $identifier): bool;
}

Це базові параметри CRUD на самому ресурсі зі зручною назвою.Звичайно, ми можемо розширити це у реалізації, якщо нам захочеться більш доступний API. Тепер давайте почнемо створювати нашу реалізацію Todoist.

declare(strict_types=1);
 
namespace App\Services\Todoist;
 
use App\Contracts\ResourceContract;
use App\Contracts\TodoContract;
use App\Services\Todoist\Resources\ProjectResource;
use App\Services\Todoist\Resources\TaskResource;
 
final class TodoistClient implements TodoContract
{
    public function __construct(
        public readonly string $url,
        public readonly string $token,
    ) {}
 
    public function projects(): ResourceContract
    {
        return new ProjectResource(
            client: $this,
        );
    }
 
    public function tasks(): ResourceContract
    {
        return new TaskResource(
            client: $this,
        );
    }
}

Я опублікую цей проект на GitHub, щоб ви могли побачити повний робочий приклад.

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

Давайте подивимося на як виглядатиме наш ProjectResource. Тоді ми зможемо проаналізувати, як це працюватиме.

declare(strict_types=1);
 
namespace App\Services\Todoist\Resources;
 
use App\Contracts\ResourceContract;
use App\Contracts\TodoContract;
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class ProjectResource implements ResourceContract
{
    public function __construct(
        private readonly TodoContract $client,
    ) {}
 
    public function list(): Collection
    {
        // TODO: Implement list() method.
    }
 
    public function get(string $identifier): DataObjectContract
    {
        // TODO: Implement get() method.
    }
 
    public function create(DataObjectContract $resource): DataObjectContract
    {
        // TODO: Implement create() method.
    }
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract
    {
        // TODO: Implement update() method.
    }
 
    public function delete(string $identifier): bool
    {
        // TODO: Implement delete() method.
    }
}

Досить проста структура, яка добре відповідає нашому інтерфейсу/контракту.Тепер ми можемо почати шукати, як ми хочемо створювати запити та надсилати їх. Мені подобається це робити, і я не соромлюся робити це по-іншому, це створити рису, яку мій ресурс використовує для надсилання запитів. Потім я можу встановити цей новий метод send у ResourceContract, щоб ресурси або використовували властивість, або мали реалізувати власний метод надсилання.Todoist API має кілька ресурсів, тому ділитися цією поведінкою має більше сенсу в межах функції. Давайте подивимося на цю властивість:

declare(strict_types=1);
 
namespace App\Services\Concerns;
 
use App\Exceptions\TodoApiException;
use App\Services\Enums\Method;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
 
trait SendsRequests
{
    public function send(
        Method $method,
        string $uri,
        null|array $data = null,
    ): Response {
        $request = $this->makeRequest();
 
        $response = $request->send(
            method: $method->value,
            url: $uri,
            options: $data ? ['json' => $data] : [],
        );
 
        if ($response->failed()) {
            throw new TodoApiException(
                response: $response,
            );
        }
 
        return $response;
    }
 
    protected function makeRequest(): PendingRequest
    {
        return Http::baseUrl(
            url: $this->client->url,
        )->timeout(
            seconds: 15,
        )->withToken(
            token: $this->client->token,
        )->withUserAgent(
            userAgent: 'todo-cli',
        );
    }
}

У нас є два методи: один створить запит, а другий надішле запит, оскільки ми хочемо використовувати стандартний спосіб для обох . Давайте тепер додамо метод send до ResourceContract, щоб застосувати цей підхід до всіх постачальників.

declare(strict_types=1);
 
namespace App\Contracts;
 
use App\Services\Enums\Method;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
interface ResourceContract
{
    public function list(): Collection;
 
    public function get(string $identifier): DataObjectContract;
 
    public function create(DataObjectContract $resource): DataObjectContract;
 
    public function update(string $identifier, DataObjectContract $payload): DataObjectContract;
 
    public function delete(string $identifier): bool;
 
    public function send(
        Method $method,
        string $uri,
        null|array $data = null,
    ): Response;
}

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

Перш ніж ми зайдемо занадто далеко з інтеграцією, напевно, настав час створити команду для входу. Зрештою, цей підручник про Laravel Zero!

Створіть нову команду, використовуючи наступне у своєму терміналі:

Ця команда потребуватиме взяття маркера API та збереження його в репозиторії конфігурації для майбутніх команд. Давайте подивимося, як працює ця команда:

php todo make:command Todo/LoginCommand

Ми вставляємо ConfigurationContract у метод handle, який вирішить конфігурацію за нас. Потім ми запитуємо маркер API як секрет, щоб він не відображався на терміналі користувача під час введення. Після очищення будь-яких поточних значень ми можемо використовувати конфігурацію, щоб установити нові значення для маркера та URL-адреси.

declare(strict_types=1);
 
namespace App\Commands\Todo;
 
use App\Contracts\ConfigurationContract;
use LaravelZero\Framework\Commands\Command;
 
final class LoginCommand extends Command
{
    protected $signature = 'login';
 
    protected $description = 'Store your API credentials for the Todoist API.';
 
    public function handle(ConfigurationContract $config): int
    {
        $token = $this->secret(
            question: 'What is your Todoist API token?',
        );
 
        if (! $token) {
            $this->warn(
                string: "You need to supply an API token to use this application.",
            );
 
            return LoginCommand::FAILURE;
        }
 
        $config->clear()->set(
            key: 'token',
            value: $token,
        )->set(
            key: 'url',
            value: 'https://api.todoist.com/rest/v1',
        );
 
        $this->info(
            string: 'We have successfully stored your API token for Todoist.',
        );
 
        return LoginCommand::SUCCESS;
    }
}

Коли ми зможемо автентифікуватися, ми можемо створити додаткову команду для переліку наших проектів. Давайте створимо це зараз:

Ця команда потребуватиме використання TodoistClient, щоб отримати всі проекти та перелічити їх у таблиці. Давайте подивимося, як це виглядає.

php todo make:command Todo/Projects/ListCommand

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

declare(strict_types=1);
 
namespace App\Commands\Todo\Projects;
 
use App\Contracts\TodoContract;
use App\DataObjects\Project;
use LaravelZero\Framework\Commands\Command;
use Throwable;
 
final class ListCommand extends Command
{
    protected $signature = 'projects:list';
 
    protected $description = 'List out Projects from the Todoist API.';
 
    public function handle(
        TodoContract $client,
    ): int {
        try {
            $projects = $client->projects()->list();
        } catch (Throwable $exception) {
            $this->warn(
                string: $exception->getMessage(),
            );
 
            return ListCommand::FAILURE;
        }
 
        $this->table(
            headers: ['ID', 'Project Name', 'Comments Count', 'Shared', 'URL'],
            rows: $projects->map(fn (Project $project): array => $project->toArray())->toArray(),
        );
 
        return ListCommand::SUCCESS;
    }
}

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

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

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