从零开始构建 PHP 命令行微框架二:控制器

PHP

介绍

MVC (Model, View, Controller) 模式在 Web 应用中非常流行。控制器负责根据从 Web 应用程序请求的终端来处理代码执行。CLI 应用程序没有终端,但我们可以通过命令控制器路由命令执行来实现类似的工作流。

本系列的第一个教程,我们已经为命令行界面(CLI)创建了一个 PHP 应用程序,使用单个入口点并通过匿名函数注册命令。在这个新的教程中,我们将重构minicli命令以使用命令控制器。

这是构建 Minicli 系列的第二章

在开始之前

你需要php-cliComposer来学习本教程。

如果你没有按照本系列的第一章节操作,你可以下载erikaheidi/minicli0.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运行应用程序,应该不会有任何更改,而且仍然可以运行hellohelp

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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/erikaheidi/php-in-the-com...

译文地址:https://learnku.com/php/t/49749

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!