从零开始构建 PHP 命令行微框架三:命名空间和自动加载
介绍
在构建 Minicli系列的前章节中,我们对初始版本的minicli
命令进行了重构,以支持类中定义的命令,具有使用命令控制器的体系结构。
在这个新的指南中,我们将实现命令命名空间来组织控制器,并创建可用于在应用程序引导期间自动加载命令的标准目录结构和命名约定。这是 web PHP 框架中常用的方法,用于简化应用程序开发,并减少引导新应用程序时所需的代码量。
我们的重构为以下步骤:
- 实现新的
CommandNamespace
类,并重构相应CommandRegistry
类。 - 将命令解析外包到一个新的
CommandCall
类。 - 更新
App
应用程序类以支持这些更改。 - 更新抽象的
CommandController
类和具体的控制器,以支持剩下的工作。 - 更新并运行
miniclili
脚本。
这是构建 Minicli 系列的第三章。
在开始之前
你需要使用php-cli
命令和 Composer 命令来学习本教程。强烈建议你从本系列的第一章开始学习,并在开始本章之前通过第二章。
如果你需要一个干净的minicli
版本来遵循本教程,请下载版本为0.1.2
的erikaheidi/minicli,以引导你的设置:
wget https://github.com/erikaheidi/minicli/archive/0.1.2.zip
unzip 0.1.2.zip
cd minicli
然后,运行 Composer 以设置自动加载。这不会安装任何软件包,因为minicli
没有依赖关系。
composer dump-autoload
使用以下命令运行应用程序:
php minicli
或者
chmod +x minicli
./minicli
1. 实现命令命名空间
在当前的应用程序设计中,每个命令都是一个单独的控制器。这是一种很好的保持命令的组织性和“协议”的方式,而不是将多个命令全部混合在单个控制器中。控制器HelloController
演示如下:
<?php
namespace App\Command;
use Minicli\CommandController;
class HelloController extends CommandController
{
public function run($argv)
{
$name = isset ($argv[2]) ? $argv[2] : "World";
$this->getApp()->getPrinter()->display("Hello $name!!!");
}
}
我们当前的CommandRegistry
维持引导应用程序时手动注册的命令控制器的记录。 getCallable
方法负责找出应用程序需要执行哪些调用:
public function getCallable($command_name)
{
$controller = $this->getController($command_name);
if ($controller instanceof CommandController) {
return [ $controller, 'run' ];
}
$command = $this->getCommand($command_name);
if ($command === null) {
throw new \Exception("Command \"$command_name\" not found.");
}
return $command;
}
这种方法的问题是,如果你有许多彼此相关但名称完全不同的命令,那么对于用户来说,它可能会变得相当混乱。
我们希望实现通用命令入口点来组织相关命令。以docker
为例:
docker image [ import | build | history | ls | pull | prune ... ]
docker container [ build | info | kill | pause | rename | rm ... ]
image
命令作为所有处理 Docker 镜像的命令的通用命名空间。同样适用于container
命令和其他docker命令。
我们将创建一个新的CommandNamespace
类,该类将在通用名称下保存应用程序控制器的注册表。然后,我们将修改CommandRegistry
类,使其直接使用 Command 命名空间,并将注册和加载控制器的工作留给这些新实体。为了在简化应用程序引导的同时进一步扩展新设计,我们将实现一个标准目录结构,以便于将命令命名空间和控制器自动加载到应用程序中。
这是我们的新架构的外观:
app/Command
└── Command1
├── DefaultController.php
├── OtherController.php
└── AnyController.php
└── Command2
└── AnotherController.php
└── Command3
└── RandomController.php
...
这是一种富有表现力的组织命令方式,同时也促进了自动加载,从而减少了为将新命令包含到应用程序中而必须编写的代码量。每个控制器都是指定命名空间下的新子命令。每个子命令的名称都是从 Controller 类名称中获取的,当命令调用中没有提供子命令时,会自动使用DefaultController。这样的目录结构将生成以下命令“map”:
./minicli command1 [ other | any ]
./minicli command2 another
./minicli command3 random
让我们从创建新的CommandNamespace
类开始。
CommandNamespace
类
使用你选择的代码编辑器在minicli/lib/CommandNamespace
中打开一个新文件。
lib/CommandNamespace.php
CommandNamespace
类将有一个参数name和一个包含映射到子命令的控制器的数组。
loadControllers
方法将利用我们定义的标准目录结构和命名约定,来创建该命名空间下所有控制器的映射。
将以下代码复制到你的CommandNamespace
类中:
<?php
namespace Minicli;
class CommandNamespace
{
protected $name;
protected $controllers = [];
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function loadControllers($commands_path)
{
foreach (glob($commands_path . '/' . $this->getName() . '/*Controller.php') as $controller_file) {
$this->loadCommandMap($controller_file);
}
return $this->getControllers();
}
public function getControllers()
{
return $this->controllers;
}
public function getController($command_name)
{
return isset($this->controllers[$command_name]) ? $this->controllers[$command_name] : null;
}
protected function loadCommandMap($controller_file)
{
$filename = basename($controller_file);
$controller_class = str_replace('.php', '', $filename);
$command_name = strtolower(str_replace('Controller', '', $controller_class));
$full_class_name = sprintf("App\\Command\\%s\\%s", $this->getName(), $controller_class);
/** @var CommandController $controller */
$controller = new $full_class_name();
$this->controllers[$command_name] = $controller;
}
}
完成后保存文件。
CommandRegistry
类
在你的编辑器上打开现有的CommandRegistry
类:
lib/CommandRegistry.php
CommandRegistry
类现在将把注册和定位控制器的工作外包给 Command 命名空间。 因为应用程序实现了标准的目录结构和命名约定,所以我们可以定位当前定义的所有命令命名空间 - 这是在autoloadNamespaces
方法中完成的。
为了保持与通过匿名函数注册的单命令的兼容性,这可能非常方便单命令应用程序,我们也将保留一个“default_registry
”数组来以这种方式注册命令。另一个重要变化是,我们现在除了getCallable
之外,还增加了一个getCallableController
。应用程序将决定使用哪一个以及何时使用。
下面是CommandRegistry
类更新后的样子:
<?php
namespace Minicli;
class CommandRegistry
{
protected $commands_path;
protected $namespaces = [];
protected $default_registry = [];
public function __construct($commands_path)
{
$this->commands_path = $commands_path;
$this->autoloadNamespaces();
}
public function autoloadNamespaces()
{
foreach (glob($this->getCommandsPath() . '/*', GLOB_ONLYDIR) as $namespace_path) {
$this->registerNamespace(basename($namespace_path));
}
}
public function registerNamespace($command_namespace)
{
$namespace = new CommandNamespace($command_namespace);
$namespace->loadControllers($this->getCommandsPath());
$this->namespaces[strtolower($command_namespace)] = $namespace;
}
public function getNamespace($command)
{
return isset($this->namespaces[$command]) ? $this->namespaces[$command] : null;
}
public function getCommandsPath()
{
return $this->commands_path;
}
public function registerCommand($name, $callable)
{
$this->default_registry[$name] = $callable;
}
public function getCommand($command)
{
return isset($this->default_registry[$command]) ? $this->default_registry[$command] : null;
}
public function getCallableController($command, $subcommand = null)
{
$namespace = $this->getNamespace($command);
if ($namespace !== null) {
return $namespace->getController($subcommand);
}
return null;
}
public function getCallable($command)
{
$single_command = $this->getCommand($command);
if ($single_command === null) {
throw new \Exception(sprintf("Command \"%s\" not found.", $command));
}
return $single_command;
}
}
更新完文件内容后保存该文件。
2. 将命令解析外包给CommandCall
类
为了方便解析命令、子命令等参数,我们将创建一个名为CommandCall
的新类。
打开一个新文件:
lib/CommandCall.php
CommandCall
类作为命令调用的简单抽象运行,并提供一种解析命名参数的方法,如:user=name
。
它很方便,因为它将这些值保存在一个类型化的对象中,这样我们就可以更好地控制转发给命令控制器的内容。将来可以对其进行扩展,以进行更复杂的解析。
CommandCall
类
将以下内容复制到你的新类CommandCall
中:
<?php
namespace Minicli;
class CommandCall
{
public $command;
public $subcommand;
public $args = [];
public $params = [];
public function __construct(array $argv)
{
$this->args = $argv;
$this->command = isset($argv[1]) ? $argv[1] : null;
$this->subcommand = isset($argv[2]) ? $argv[2] : 'default';
$this->loadParams($argv);
}
protected function loadParams(array $args)
{
foreach ($args as $arg) {
$pair = explode('=', $arg);
if (count($pair) == 2) {
$this->params[$pair[0]] = $pair[1];
}
}
}
public function hasParam($param)
{
return isset($this->params[$param]);
}
public function getParam($param)
{
return $this->hasParam($param) ? $this->params[$param] : null;
}
}
完成后保存文件。
3. 更新App
类
为了适应CommandRegistry
中的更改,我们还需要更新App
类。使用以下命令打开文件:
lib/App.php
runCommand
方法现在先调用CommandRegistry
类中的getCallableController
方法;如果找到控制器,它会按顺序执行三个不同的方法:boot
、run
和teardown
。如果找不到控制器,可能意味着命名空间不存在,这实际上是一个命令。我们将尝试查找单个命令并运行其各自的可调用命令,否则应用程序将退出并返回错误。
还有一个新的app_signature
属性,可以让我们定制一行程序来告诉人们如何使用这款应用程序。
App
类
以下是更新后的App
类的内容:
<?php
namespace Minicli;
class App
{
protected $printer;
protected $command_registry;
protected $app_signature;
public function __construct()
{
$this->printer = new CliPrinter();
$this->command_registry = new CommandRegistry(__DIR__ . '/../app/Command');
}
public function getPrinter()
{
return $this->printer;
}
public function getSignature()
{
return $this->app_signature;
}
public function printSignature()
{
$this->getPrinter()->display(sprintf("usage: %s", $this->getSignature()));
}
public function setSignature($app_signature)
{
$this->app_signature = $app_signature;
}
public function registerCommand($name, $callable)
{
$this->command_registry->registerCommand($name, $callable);
}
public function runCommand(array $argv = [])
{
$input = new CommandCall($argv);
if (count($input->args) < 2) {
$this->printSignature();
exit;
}
$controller = $this->command_registry->getCallableController($input->command, $input->subcommand);
if ($controller instanceof CommandController) {
$controller->boot($this);
$controller->run($input);
$controller->teardown();
exit;
}
$this->runSingle($input);
}
protected function runSingle(CommandCall $input)
{
try {
$callable = $this->command_registry->getCallable($input->command);
call_user_func($callable, $input);
} catch (\Exception $e) {
$this->getPrinter()->display("ERROR: " . $e->getMessage());
$this->printSignature();
exit;
}
}
}
更新完文件内容后,保存文件。
4. 重构抽象类和具体命令控制器
现在是时候更新由我们的控制器继承的抽象类,以便包括一些方便的方法来检索参数并用作访问应用程序组件(如打印机)的快捷方式。
打开 CommandController
类:
lib/CommandController.php
根据新的「contract」, 控制器必须实现 handle
方法。从外部看,什么都不会改变:run
仍然是将从 APP
类执行的公共方法。所做的更改是启用对 CommandCall
数据的拦截并使其可用于所有受保护的控制器方法。
teardown
方法是可选的,因此是空的,所以可以在控制器中覆盖它。
CommandController
类
下面是更新后的 CommandController
抽象类:
<?php
namespace Minicli;
abstract class CommandController
{
protected $app;
protected $input;
abstract public function handle();
public function boot(App $app)
{
$this->app = $app;
}
public function run(CommandCall $input)
{
$this->input = $input;
$this->handle();
}
public function teardown()
{
//
}
protected function getArgs()
{
return $this->input->args;
}
protected function getParams()
{
return $this->input->params;
}
protected function hasParam($param)
{
return $this->input->hasParam($param);
}
protected function getParam($param)
{
return $this->input->getParam($param);
}
protected function getApp()
{
return $this->app;
}
protected function getPrinter()
{
return $this->getApp()->getPrinter();
}
}
更新完文件内容后,请保存该文件。
我们需要移动当前的 hello
命令,以遵循制定的目录结构:
cd minicli
mkdir app/Command/Hello
因为我们现在使用 command subcommand
命名法,所以我们必须在 hello
命名空间内创建一个子命令。要创建名为 name
的子命令,应使用 NameController
做为类名。
让我们将 HelloController
复制到 hello
命名空间并将其重命名为 NameController.php
。
mv app/Command/HelloController.php app/Command/Hello/NameController.php
现在我们需要更新此文件以重命名该类并实现 handle
方法,删除旧的 run
实现。使用以下方式打开文件:
app/Hello/NameController.php
NameController
类
下面是更新的 NameController
类的内容,以前是 HelloController
。
<?php
namespace App\Command\Hello;
use Minicli\CommandController;
class NameController extends CommandController
{
public function handle()
{
$name = $this->hasParam('user') ? $this->getParam('user') : 'World';
$this->getPrinter()->display(sprintf("Hello, %s!", $name));
}
}
更新完文件内容后,保存文件。
5. 更新并运行 minicli
我们要做的最后一件事是更新 minicli
脚本以反映所有更改。我们将设置一个签名并注册一个 help
命令以测试我们的命名参数功能。
打开下面的文件:
cd minicli
nano minicli
minicli
脚本
用以下代码替换 minicli
脚本的当前内容:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
use Minicli\CommandCall;
$app = new App();
$app->setSignature("minicli hello name [ user=name ]");
$app->registerCommand("help", function(CommandCall $call) use ($app) {
$app->printSignature();
print_r($call->params);
});
$app->runCommand($argv);
完成后保存文件。
8. 测试更改
现在你可以使用以下命令执行 hello name
命令:
./minicli hello name
或者
./minicli hello name user=erika
要测试 name 参数,请运行
./minicli help name=value name2=value2
你将获得如下输出:
usage: minicli hello name [ user=name ]
Array
(
[name] => value
[name2] => value2
)
总结
在本指南中,我们重构了 minicli
微型框架以支持更好的组织命令结构并启用自动加载命令控制器。
你可以在 minicli
的 0.1.3 版本中找到完整的重构代码:github.com/erikaheidi/minicli/rele...。
在本系列的下篇也是最后一步分钟,我们将总结所有内容以发布 minicli 1.0
。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: