• Время чтения ~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-приложение, которое позволит нам видеть проекты и задачи в моем 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, которые мы хотим использовать. Прежде чем мы создадим команды, нам нужно создать класс, который интегрируется с API Todoist. Опять же, я создам для этого интерфейс/контракт, если решу перейти с 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, чтобы ресурсы либо использовали трейт, либо должны были реализовать свой собственный метод отправки.В API Todoist есть несколько ресурсов, поэтому лучше использовать это поведение в рамках трейта. Давайте посмотрим на этот трейт:

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 в метод дескриптора, который разрешит для нас конфигурацию. Затем мы запрашиваем токен 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, вы увидите, что list в 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