使用 Laravel Zero 创建无忧 CLI 应用程序
最后更新于 作者: Steve McDougall
怎么说呢?CLI 应用程序很酷。能够在任何地方打开终端,只需运行一条命令就可以完成一项可能需要更长时间才能完成的任务。打开浏览器,访问正确的页面,登录并找到你需要做的事情,等待页面加载... 你懂的。
在过去的几年里,命令终端得到了大量的投入;从 ZSH 到自动完成,从 FIG 到 Warp - CLI 是我们无法逃避的东西。我构建 CLI 应用程序是为了帮助我更有效地完成小任务,或者按计划完成工作。
每当我查看任何与 Laravel 相关的在线内容时,它总是与 Web 应用程序有关,而且这是有道理的。毕竟,Laravel 是一个很棒的 Web 应用程序框架!但是,利用我们对 Laravel 的热爱,也可以用于 CLI 应用程序。现在,我们可以使用完整的 Laravel 安装并运行调度器来运行我们需要的 Artisan 命令 - 但这有时会显得过于复杂。如果你不需要 Web 界面,你就不需要 Laravel。相反,让我们谈谈 Laravel Zero,这是 Nuno Maduro 的另一个杰作。
Laravel Zero 将自己描述为“用于控制台应用程序的微型框架” - 这很准确。它允许你使用经过验证的框架构建 CLI 应用程序 - 比使用像 Laravel 这样的东西更小巧。它文档齐全,健壮且积极维护 - 使其成为构建任何 CLI 应用程序的完美选择。
在本教程中,我将带你逐步完成一个使用 Laravel Zero 的简单示例,希望它能让你看到它有多么有用。我们将构建一个 CLI 应用程序,使我们能够查看我的 Todoist 帐户中的项目和任务,这样我就不必打开应用程序或 Web 浏览器。
首先,我们需要转到 Todoist 的 Web 应用程序并打开集成设置以获取我们的 API 令牌。我们稍后会用到它。第一步是创建一个新的 Laravel Zero 项目,我们可以使用它。
composer create-project --prefer-dist laravel-zero/laravel-zero todoist
在你的 IDE 中打开这个新项目,以便我们开始构建我们的 CLI 应用程序。我们知道的第一件事是,我们想要存储我们的 API 令牌,因为我们不想每次想要运行新命令时都必须粘贴它。这里的一种典型方法是将 API 令牌存储在用户的 home 目录中,位于隐藏目录中的配置文件中。所以我们将看看如何实现这一点。
我们想要创建一个 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;}
我们有两个方法,projects
和 tasks
,它们将为我们返回一个资源类。像往常一样,这个资源类需要一个契约。资源契约将使用数据对象契约,但与其创建它,我将使用我构建在我的一个包中的一个契约。
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
请求的 Trait。然后,我可以在 ResourceContract
上设置这个新的 send
方法,以便资源要么使用该 Trait,要么必须实现自己的 send 方法。Todoist API 有几种资源,所以在这个 Trait 中共享这种行为更有意义。让我们看看这个 Trait。
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', ); }}
相关文章:[PHP 用户代理解析器[(https://news.laravel.net.cn/php-desktop-and-mobile-user-agent-parser)
我们有两个方法,一个用于构建请求,一个用于发送请求 - 因为我们想要一种标准的方式来执行这两个操作。现在,让我们在 ResourceContract
上添加 send
方法,以强制在所有提供商中使用这种方法。
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;}
现在,我们的资源要么必须创建自己的创建和发送请求的方式,要么可以实现这个 Trait。正如你从代码示例中看到的,我为请求方法创建了一个辅助 Enum - 此代码在存储库中,所以你可以随意深入查看代码以获取更多信息。
在我们深入集成方面之前,我们可能应该创建一个用于登录的命令。毕竟,本教程是关于 Laravel Zero 的!
使用以下命令在你的终端中创建一个新的命令。
php todo make:command Todo/LoginCommand
此命令将需要获取 API 令牌并将其存储在配置存储库中,以便在将来的命令中使用。让我们看看这个命令是如何工作的。
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; }}
我们将 ConfigurationContract
注入到 handle 方法中,它将为我们解析配置。然后,我们请求一个 API 令牌作为秘密,这样它就不会在用户键入时显示在他们的终端上。在清除任何当前值后,我们可以使用配置来设置令牌和 URL 的新值。
一旦我们能够进行身份验证,我们就可以创建一个额外的命令来列出我们的项目。让我们现在创建它。
php todo make:command Todo/Projects/ListCommand
此命令将需要使用 TodoistClient
来获取所有项目并在表格中列出它们。让我们看看它是什么样子的。
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; }}
如果你查看 GitHub 仓库中的代码,你会发现 ProjectResource
上的 list
命令返回了一个 Project
数据对象的集合。这使我们能够映射集合中的每个项目,将对象转换为数组,并将集合作为数组返回,这样我们就可以轻松地以表格格式查看我们拥有的项目。使用合适的终端,我们还可以点击项目的 URL,在浏览器中打开它,如果需要的话。
如你从以上方法中看到的那样,使用 Laravel Zero 构建 CLI 应用程序非常简单 - 你能构建的唯一限制是你的想象力。
正如本教程中提到的,你可以在 GitHub 仓库在线查看,因此你可以克隆完整的示例。