从零开始构建 PHP 命令行微框架三:命名空间和自动加载

PHP

介绍

构建 Minicli系列的前章节中,我们对初始版本的minicli命令进行了重构,以支持类中定义的命令,具有使用命令控制器的体系结构。

在这个新的指南中,我们将实现命令命名空间来组织控制器,并创建可用于在应用程序引导期间自动加载命令的标准目录结构和命名约定。这是 web PHP 框架中常用的方法,用于简化应用程序开发,并减少引导新应用程序时所需的代码量。

我们的重构为以下步骤:

  1. 实现新的CommandNamespace类,并重构相应CommandRegistry类。
  2. 将命令解析外包到一个新的CommandCall类。
  3. 更新App应用程序类以支持这些更改。
  4. 更新抽象的CommandController类和具体的控制器,以支持剩下的工作。
  5. 更新并运行miniclili脚本。

这是构建 Minicli 系列的第三章

在开始之前

你需要使用php-cli命令和 Composer 命令来学习本教程。强烈建议你从本系列的第一章开始学习,并在开始本章之前通过第二章

如果你需要一个干净的minicli版本来遵循本教程,请下载版本为0.1.2erikaheidi/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方法;如果找到控制器,它会按顺序执行三个不同的方法:bootrunteardown。如果找不到控制器,可能意味着命名空间不存在,这实际上是一个命令。我们将尝试查找单个命令并运行其各自的可调用命令,否则应用程序将退出并返回错误。

还有一个新的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 微型框架以支持更好的组织命令结构并启用自动加载命令控制器。

你可以在 minicli0.1.3 版本中找到完整的重构代码:github.com/erikaheidi/minicli/rele...

在本系列的下篇也是最后一步分钟,我们将总结所有内容以发布 minicli 1.0

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/erikaheidi/building-minic...

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

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

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