在 Vanilla PHP 中引导 CLI PHP 应用程序
介绍
PHP 因其在 Web 应用程序和 CMS 中的流行而闻名,但许多人不知道的是,PHP 也是一门非常适合构建无需 Web 服务器的命令行应用程序的语言。它的易用性和熟悉的语法使其成为一种低门槛的语言,可以用于开发免费工具和小型应用程序,例如与 API 通信或通过 Crontab 执行计划任务,而无需暴露给外部用户。
当然,您可以编写一个单文件 PHP 脚本来满足您的需求,这对于一些小事情来说可能效果不错;但这会使将来维护、扩展或重用该代码变得非常困难。构建命令行应用程序时可以应用相同的 Web 开发原则,只不过我们不再使用前端了——太棒了!此外,外部用户无法访问该应用程序,这增加了安全性,并为实验创造了更大的空间。
最近我有点厌倦了 Web 应用以及围绕前端构建的复杂性,所以在命令行中玩转 PHP 对我个人来说非常新鲜。在本系列文章中,我们将共同构建一个极简主义/无依赖的 CLI AppKit(可以想象成一个微型框架)—— minicli,它可以作为你用 PHP 开发实验性 CLI 应用的基础。
附言:如果您需要的只是一个git clone
,请转到此处。
这是构建 Minicli系列的第 1 部分。
先决条件
为了遵循本教程,您需要php-cli
在本地计算机或开发服务器上安装Composer来生成自动加载文件。
1. 设置目录结构和入口点
让我们首先创建主项目目录:
mkdir minicli
cd minicli
接下来,我们将为 CLI 应用程序创建入口点。这相当于index.php
现代 PHP Web 应用程序中的入口点文件,其中单个入口点将请求重定向到相关的控制器。但是,由于我们的应用程序仅包含 CLI,因此我们将使用不同的文件名,并包含一些安全措施,以阻止从 Web 服务器执行。
minicli
使用您喜欢的文本编辑器打开一个名为的新文件:
vim minicli
你会注意到我们这里没有包含.php
扩展名。因为我们在命令行上运行这个脚本,所以我们可以添加一个特殊的描述符来告诉你的 shell 程序我们正在使用 PHP 来执行这个脚本。
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
echo "Hello World\n";
第一行是应用程序shebang。它告诉运行此脚本的 shell 使用这个脚本/usr/bin/php
作为该代码的解释器。
使用以下命令使脚本可执行chmod
:
chmod +x minicli
现在您可以使用以下命令运行该应用程序:
./minicli
您应该看到Hello World
输出。
2. 设置源目录和自动加载
为了便于在多个应用程序中重复使用此框架,我们将创建两个源目录:
app
:此命名空间将保留给特定于应用程序的模型和控制器。lib
:此命名空间将被核心框架类使用,可在各种应用程序中重复使用。
使用以下命令创建两个目录:
mkdir app
mkdir lib
现在让我们创建一个composer.json
文件来设置autoload。这将帮助我们在使用 PHP 中的类和其他面向对象资源时更好地组织我们的应用程序。
在文本编辑器中创建一个新composer.json
文件并包含以下内容:
{
"autoload": {
"psr-4": {
"Minicli\\": "lib/",
"App\\": "app/"
}
}
}
保存并关闭文件后,运行以下命令设置自动加载文件:
composer dump-autoload
为了测试自动加载功能是否按预期工作,我们将创建第一个类。该类将代表 Application 对象,负责处理命令的执行。我们尽量简单,将其命名为App
。
使用您选择的文本编辑器在您的文件夹中创建一个新App.php
文件:lib
vim lib/App.php
该类App
实现了一个runCommand
方法,用于替换我们之前在可执行文件中设置的“Hello World”代码minicli
。
稍后我们将修改此方法,使其能够处理多个命令。目前,它将使用执行脚本时传递的参数输出“Hello $name”文本;如果没有传递参数,它将使用world
该变量的默认值$name
。
在文件中插入以下内容App.php
,完成后保存并关闭文件:
<?php
namespace Minicli;
class App
{
public function runCommand(array $argv)
{
$name = "World";
if (isset($argv[1])) {
$name = $argv[1];
}
echo "Hello $name!!!\n";
}
}
现在转到您的minicli
脚本并用以下代码替换当前内容,我们将在稍后解释:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
$app = new App();
$app->runCommand($argv);
这里,我们需要自动生成的autoload.php
文件,以便在创建新对象时自动包含类文件。创建App
对象后,我们调用该runCommand
方法,并传递一个全局$argv
变量,该变量包含运行该脚本时使用的所有参数。该$argv
变量是一个数组,其中第一个位置 (0) 是脚本的名称,后续位置是传递给命令调用的额外参数。这是一个预定义变量,可在从命令行执行的 PHP 脚本中使用。
现在,为了测试一切是否按预期工作,请运行:
./minicli your-name
您应该看到以下输出:
Hello your-name!!!
现在,如果您不向脚本传递任何其他参数,它应该打印:
Hello World!!!
3. 创建输出助手
由于命令行界面是纯文本的,有时很难识别应用程序中的错误或警报消息,或者难以以更易于阅读的方式格式化数据。我们将其中一些任务外包给一个辅助类,该类负责处理终端的输出。
lib
使用您选择的文本编辑器在文件夹内创建一个新类:
vim lib/CliPrinter.php
以下类定义了三个公共方法:一个out
用于输出消息的基本方法;一个newline
用于打印新行的方法;以及一个display
将这两个方法结合起来以强调文本(用新行换行)的方法。稍后我们将扩展此类以包含更多格式选项。
<?php
namespace Minicli;
class CliPrinter
{
public function out($message)
{
echo $message;
}
public function newline()
{
$this->out("\n");
}
public function display($message)
{
$this->newline();
$this->out($message);
$this->newline();
$this->newline();
}
}
现在让我们更新该类App
以使用 CliPrinter 辅助类。我们将创建一个名为 的属性,$printer
该属性将引用一个CliPrinter
对象。该对象是在App
构造函数方法中创建的。然后,我们将创建一个getPrinter
方法,并在方法中使用它runCommand
来显示消息,而不是echo
直接使用:
<?php
namespace Minicli;
class App
{
protected $printer;
public function __construct()
{
$this->printer = new CliPrinter();
}
public function getPrinter()
{
return $this->printer;
}
public function runCommand($argv)
{
$name = "World";
if (isset($argv[1])) {
$name = $argv[1];
}
$this->getPrinter()->display("Hello $name!!!");
}
}
现在再次运行该应用程序:
./minicli your_name
您应该会得到如下输出(消息周围有换行符):
Hello your_name!!!
在下一步中,我们将把命令逻辑移到App
类之外,以便您在需要时更轻松地包含新命令。
4.创建命令注册表
现在,我们将重构该类,使其能够通过一个通用方法和一个命令注册表App
来处理多个命令。新命令的注册方式与一些流行的 PHP Web 框架中定义的路由方式类似。runCommand
更新后的App
类现在将包含一个新属性,即一个名为 的数组command_registry
。该方法registerCommand
将使用此变量将应用程序命令存储为由名称标识的匿名函数。
该runCommand
方法现在会检查 是否$argv[1]
设置为已注册的命令名。如果未设置命令,它将默认尝试执行help
命令。如果未找到有效命令,它将打印错误消息。
修改后,更新后的类如下所示App.php
。将文件的当前内容替换App.php
为以下代码:
<?php
namespace Minicli;
class App
{
protected $printer;
protected $registry = [];
public function __construct()
{
$this->printer = new CliPrinter();
}
public function getPrinter()
{
return $this->printer;
}
public function registerCommand($name, $callable)
{
$this->registry[$name] = $callable;
}
public function getCommand($command)
{
return isset($this->registry[$command]) ? $this->registry[$command] : null;
}
public function runCommand(array $argv = [])
{
$command_name = "help";
if (isset($argv[1])) {
$command_name = $argv[1];
}
$command = $this->getCommand($command_name);
if ($command === null) {
$this->getPrinter()->display("ERROR: Command \"$command_name\" not found.");
exit;
}
call_user_func($command, $argv);
}
}
接下来,我们将更新minicli
脚本并注册两个命令:hello
和。它们将使用新创建的方法help
在我们的对象中注册为匿名函数。App
registerCommand
复制更新后的minicli
脚本并更新您的文件:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
$app = new App();
$app->registerCommand('hello', function (array $argv) use ($app) {
$name = isset ($argv[2]) ? $argv[2] : "World";
$app->getPrinter()->display("Hello $name!!!");
});
$app->registerCommand('help', function (array $argv) use ($app) {
$app->getPrinter()->display("usage: minicli hello [ your-name ]");
});
$app->runCommand($argv);
现在你的应用程序有两个可以运行的命令:help
和hello
。要测试它,请运行:
./minicli help
这将打印:
usage: minicli hello [ your-name ]
现在hello
使用以下命令测试该命令:
./minicli hello your_name
Hello your_name!!!
您现在有一个使用极简结构的工作 CLI 应用程序,它将作为实现更多命令和功能的基础。
此时您的目录结构将如下所示:
.
├── app
├── lib
│ ├── App.php
│ └── CliPrinter.php
├── vendor
│ ├── composer
│ └── autoload.php
├── composer.json
└── minicli
在本系列的下一篇中,我们将重构minicli
以使用命令控制器,将命令逻辑移至应用程序特定命名空间内的专用类。下次再见!