从零开始构建 PHP 命令行微框架一:基础结构
介绍
PHP 因其在 Web 应用程序和 CMS 中很受欢迎而广为人知, 但很多人不知道的是,PHP 也是一门伟大的语言,因为它不需要 web 服务器也能构建命令行应用程序。它的易用性和熟悉的语法使它成为免费工具和小应用程序使用的低门槛语言,例如,这些工具和小应用程序可以与 API 通信或者通过 Crontab 执行计划的任务,而无需向外部用户公开。
当然,你可以构建一个只有一个文件的 PHP 脚本来满足你的需要,这对于一些小事情来说可能运行得不错;但这使得将来维护、扩展或重用该代码变得非常困难。构建命令行应用程序时也可以应用相同的 Web 开发原则,只是我们不再使用前端了-耶!此外,外部用户不能访问该应用程序,这增加了安全性,并为实验创造了更多空间。
最近,我有点厌倦了 Web 应用程序和围绕前端构建的复杂性,所以在命令行中操作 PHP 对我个人来说是很新鲜的。在这篇文章/系列中,我们将共同构建一个极简主义的/无依赖的 CLI AppKit(想下一个很小的框架)- minicli - 它可以用作 PHP 中实验性 CLI 应用程序的基础。
PS.: 如果你所需要的只是 git clone
, 请到这里。
这是构建 Minicli 系列的 第一章。
前提条件
为了学习本教程,你需要在本地计算机或开发服务器上安装php-cli
,并且用于生成自动加载文件的 Composer 。
1. 设置目录结构和入口点
让我们从创建主项目目录开始:
mkdir minicli
cd minicli
接下来,我们将为 CLI 应用程序创建入口点。这相当于现代 PHP Web 应用程序上的一个index.php
文件,在该文件中,单个入口点将请求重定向到相关控制器。但是,由于我们的应用程序只是 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
: 此命名空间将保留给特定应用程序的 model 和 controller。lib
: 此命名空间将由核心框架类使用,这些核心框架类可以在各种应用程序中重用。
使用以下命令创建两个目录:
mkdir app
mkdir lib
现在,让我们创建一个名为composer.json
的文件来设置 autoload。这将帮助我们在使用 PHP 中的类和其他面向对象的资源时,更好地组织我们的应用程序。
- 在你的文本编辑器中创建一个新的
composer.json
文件,并包含以下内容:
{
"autoload": {
"psr-4": {
"Minicli\\": "lib/",
"App\\": "app/"
}
}
}
保存并关闭文件后,运行以下命令设置自动加载文件:
composer dump-autoload
为了测试自动加载是否按预期工作,我们将创建第一个类。此类将表示 Application 对象,负责处理命令执行。我们将保持它的简单性,并将其命名为App
。
使用你选择的文本编辑器,在lib
文件夹中创建一个新的App.php
文件:
vim lib/App.php
App
类实现了一个runCommand
方法,代替了我们之前在minicli
可执行文件中设置的“Hello World”代码。
我们稍后将修改此方法,以便它可以处理多个命令。目前,它将使用执行脚本时传递的参数输出“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
命令类,通过通用的runCommand
方法和命令注册表处理多个命令。 新命令将被注册,就像在一些流行的 PHP web 框架中通常定义的routes一样。
现在,更新后的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
。这些函数将使用新创建的registerCommand
方法在我们的App
应用程序对象中注册为匿名函数。
复制更新后的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
类以使用命令控制器,将命令逻辑移动到特定应用程序的命名空间内的专用类。下次见!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。