我怎样才能把这个?CLI应用程序是很酷的。能够在任何地方打开终端,只需运行一个命令,就能完成一项可能需要更长时间的工作。打开浏览器进入正确的页面,登录并找到你需要做的事情,然后等待页面加载....。你会明白的。
在过去的几年里,命令终端有很多投资;从ZSH到自动完成,从FIG到Warp--CLI是我们无法逃避的东西。我建立CLI应用程序是为了帮助我更有效地完成小任务或按计划完成一项工作。
每当我在网上看任何与Laravel有关的东西, 它总是一个web应用程序, 这是有道理的.Laravel毕竟是一个神奇的网络应用程序框架!然而, 利用我们对Laravel的喜爱,也可以用于CLI应用。现在我们可以使用一个完整的Laravel安装,并在需要的地方运行调度器来运行artisan命令 - 但这有时是矫枉过正。如果你不需要一个网络界面, 你就不需要Laravel.相反, 让我们来谈谈Laravel Zero,Nuno Maduro的另一个创意.
Laravel Zero将自己描述为一个 "控制台应用程序的微型框架" - 这是很准确的。它允许你使用一个成熟的框架来构建CLI应用程序 - 这比使用Laravel这样的东西要小。它有很好的文档,坚固耐用,并积极维护 - 使它成为你可能想要建立的任何CLI应用程序的完美选择。
在本教程中, 我将通过一个有点简单的例子来介绍Laravel Zero的使用, 希望它能向你展示它的实用性.我们将建立一个CLI应用程序,使我们能够看到我的Todoist帐户中的项目和任务,这样我就不必打开一个应用程序或Web浏览器。
要开始,我们需要进入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;
}
我们有两个方法,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和token,这就是为什么这些属性被保护而不是私有的原因。
让我们看看我们的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 ,设置这个新的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',
);
}
}
我们有两个方法,一个是建立请求,一个是发送请求--因为我们希望有一个标准的方法来做这两件事。现在让我们把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的!
在你的终端中使用以下内容创建一个新的命令:
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上存储库中的代码,你会发现list 命令在ProjectResource 上返回一个Project 数据对象的集合。这使我们能够映射集合中的每个项目,将对象铸成数组,并将集合作为数组返回,因此我们可以很容易地以表格的形式看到我们有哪些项目。使用右边的终端,如果需要的话,我们还可以点击项目的URL,在浏览器中打开这个项目。
从上面的方法可以看出, 使用Laravel Zero建立一个CLI应用程序是非常简单的 - 你可以建立的唯一限制是你的想象力。