从零开始构建 PHP 命令行微框架二:控制器
介绍
MVC (Model, View, Controller) 模式在 Web 应用中非常流行。控制器负责根据从 Web 应用程序请求的终端来处理代码执行。CLI 应用程序没有终端,但我们可以通过命令控制器路由命令执行来实现类似的工作流。
在本系列的第一个教程,我们已经为命令行界面(CLI)创建了一个 PHP 应用程序,使用单个入口点并通过匿名函数注册命令。在这个新的教程中,我们将重构minicli
命令以使用命令控制器。
这是构建 Minicli 系列的第二章。
在开始之前
你需要php-cli
和Composer来学习本教程。
如果你没有按照本系列的第一章节操作,你可以下载erikaheidi/minicli的0.1.0
版本来开始你的设置:
wget https://github.com/erikaheidi/minicli/archive/0.1.0.zip
unzip 0.1.0.zip
cd minicli
然后,运行 Composer 以设置自动加载。这不会安装任何软件包,因为minicli
没有依赖关系。
composer dump-autoload
使用以下命令运行应用程序:
php minicli
或者
chmod +x minicli
./minicli
1. 将命令注册外包给一个CommandRegistry
类
为了开始重构,我们将创建一个新类来处理为应用程序注册和定位命令的工作。这项工作目前由App
命令类来处理,但我们会将其外包给一个名为CommandRegistry
的类。
使用你选择的编辑器创建新类。为简单起见,在本教程中,我们将使用nano
:
nano lib/CommandRegistry.php
将以下内容复制到你的CommandRegistry
类中:
<?php
namespace Minicli;
class CommandRegistry
{
protected $registry = [];
public function registerCommand($name, $callable)
{
$this->registry[$name] = $callable;
}
public function getCommand($command)
{
return isset($this->registry[$command]) ? $this->registry[$command] : null;
}
}
注:getCommand
方法使用三元运算符作为 if/else 的简写。 如果找不到命令,则返回null
命令。
完成后保存并关闭该文件。
现在,编辑文件App.php
,将当前内容替换为以下代码,其中包含了用于注册命令的CommandRegistry
命令类:
<?php
namespace Minicli;
class App
{
protected $printer;
protected $command_registry;
public function __construct()
{
$this->printer = new CliPrinter();
$this->command_registry = new CommandRegistry();
}
public function getPrinter()
{
return $this->printer;
}
public function registerCommand($name, $callable)
{
$this->command_registry->register($name, $callable);
}
public function runCommand(array $argv = [])
{
$command_name = "help";
if (isset($argv[1])) {
$command_name = $argv[1];
}
$command = $this->command_registry->getCommand($command_name);
if ($command === null) {
$this->getPrinter()->display("ERROR: Command \"$command_name\" not found.");
exit;
}
call_user_func($command, $argv);
}
}
如果现在使用./minicli
运行应用程序,应该不会有任何更改,而且仍然可以运行hello
和help
。
2. 实现命令控制器
现在我们将进一步重构命令,将特定的命令过程移到专门的CommandController
类。
2.1 创建一个CommandController
模型
我们需要做的第一件事是建立一个可以由几个命令继承的抽象模型。这允许我们拥有一些默认实现,同时通过需要由子(具体)类实现的抽象方法来实施一组功能。
此模型应定义至少一个强制方法,当用户在命令行上调用该命令时,该方法将由给定的具体命令CommandController
上的App
命令类调用。
在你的文本编辑器打开一个新文件:
nano lib/CommandController.php
将以下内容复制到此文件。这就是我们初始的CommandController
抽象类的样子:
<?php
namespace Minicli;
abstract class CommandController
{
protected $app;
abstract public function run($argv);
public function __construct(App $app)
{
$this->app = $app;
}
protected function getApp()
{
return $this->app;
}
}
任何继承CommandController
的类都将继承getApp
方法,但是将必需实现run
方法并处理命令行执行。
2.2 创建具体的命令控制器
现在我们将创建我们的第一个命令行控制器具体类:HelloController
。此类将从匿名函数替换hello
命令的当前定义到CommandController
对象。
还记得我们如何在我们的Composer文件中创建两个单独的命名空间,一个用于框架,另一个用于应用程序吗?由于此代码非常明确于正在开发的应用程序,因此我们现在将使用App
命名空间。
首先,在 app
命名空间目录下创建一个名为 Command
的新文件夹:
mkdir app/Command
在文本编辑器中打开一个新文件:
nano app/Command/HelloController.php
将以下内容复制到你的控制器中。新的 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!!!");
}
}
这里没有过多操作。我们复用了之前的相同代码。但现在将其放在一个独立的类中,该类继承自 CommandController
。现在可以通过从父抽象类 CommandController
继承的方法 getApp
访问 App
对象。
2.3 更新 CommandRegistry
以使用控制器
我们已经基于继承为命令控制器定义了一个简单的体系结构,但是我们仍然需要更新 CommandRegistry
类以处理这些更改。
能够将命令分离到它们自己的类中非常有利于维护,但是对于简单的命令,你可能仍然更喜欢使用匿名函数。
下面的代码实现了命令控制器的注册,其方式与以前使用匿名函数定义命令的方法保持兼容。使用您选择的编辑器打开 CommandRegistry.php
文件:
nano lib/CommandRegistry.php
使用以下代码更新 CommandRegistry
类的当前内容:
<?php
namespace Minicli;
class CommandRegistry
{
protected $registry = [];
protected $controllers = [];
public function registerController($command_name, CommandController $controller)
{
$this->controllers = [ $command_name => $controller ];
}
public function registerCommand($name, $callable)
{
$this->registry[$name] = $callable;
}
public function getController($command)
{
return isset($this->controllers[$command]) ? $this->controllers[$command] : null;
}
public function getCommand($command)
{
return isset($this->registry[$command]) ? $this->registry[$command] : null;
}
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;
}
}
由于我们现在在应用程序中同时注册了命令控制器和简单的回调函数,因此我们实现了名为 getCallable
的方法,该方法将负责确定调用命令时应调用哪个代码。万一找不到命令,此方法将引发异常。我们实现它的方式,命令控制器将始终优先于通过匿名函数注册的单个命令。
替换掉旧代码,保存并关闭文件。
2.4 更新 App
类
我们仍然需要更新 App
类以处理所有最近的更改。
打开包含 App
类的文件:
nano lib/App.php
用以下代码替换 App.php
文件当前的内容:
<?php
namespace Minicli;
class App
{
protected $printer;
protected $command_registry;
public function __construct()
{
$this->printer = new CliPrinter();
$this->command_registry = new CommandRegistry();
}
public function getPrinter()
{
return $this->printer;
}
public function registerController($name, CommandController $controller)
{
$this->command_registry->registerController($name, $controller);
}
public function registerCommand($name, $callable)
{
$this->command_registry->registerCommand($name, $callable);
}
public function runCommand(array $argv = [], $default_command = 'help')
{
$command_name = $default_command;
if (isset($argv[1])) {
$command_name = $argv[1];
}
try {
call_user_func($this->command_registry->getCallable($command_name), $argv);
} catch (\Exception $e) {
$this->getPrinter()->display("ERROR: " . $e->getMessage());
exit;
}
}
}
首先,我们已经实现了一种方法,通过实例化一个App对象来实现允许用户注册命令控制器: registerController
。这个方法会将命令注册外包给 CommandRegistry
对象。之后,我们更新了 runCommand
方法以使用getCallable
,并在try / catch块中捕获可能的异常。
完成编辑后,保存并关闭文件。
2.5 注册 HelloController
命令控制器
minicli
脚本使用的依然是通过匿名函数定义命令的旧方法。 现在我们将更新此文件以使用新的 HelloController
命令控制器,但我们将继续保持 help
命令注册的方式就像之前一样, 将其注册为匿名函数。
打开 minicli
脚本:
nano minicli
这就是更新后的 minicli
脚本现在的样子:
#!/usr/bin/php
<?php
if (php_sapi_name() !== 'cli') {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use Minicli\App;
$app = new App();
$app->registerController('hello', new \App\Command\HelloController($app));
$app->registerCommand('help', function (array $argv) use ($app) {
$app->getPrinter()->display("usage: minicli hello [ your-name ]");
});
$app->runCommand($argv);
使用新代码更新文件后,你应该能够像之前一样运行应用程序,并且它的行为应该完全相同:
./minicli
不同的是,现在你有两种创建命令的方式:通过使用registerCommand
注册匿名函数,或者通过创建从CommandController
继承的 Controller 类。使用 Controller 类将使你的代码更具组织性和可维护性,但是你仍然可以使用带有匿名函数的“捷径”来快速破解和编写简单的脚本。
结论 & 下一步
在这篇文章中,我们重构了minicli
命令,以支持类中定义的命令,架构使用了Command Controllers。虽然这目前运行良好,但控制器应该应该能够处理多个命令;这将使我们更容易实现如下命令结构:
command [ subcommand ] [ action ] [ params ]
command [ subcommand 1 ] [ subcommand n ] [ params ]
在本系列的下一部分中,我们将重构 minicli
以支持子命令。
你怎么看?你又将如何实现呢?
期待下一次相见! \,,/
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
本教程中使用的所有文件都可以在这里找到: erikaheidi/minicli:v0.1.2
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。