toobo/sealion

命令行界面(CLI)路由器。

0.1.0 2015-02-16 20:59 UTC

This package is auto-updated.

Last update: 2024-08-29 03:58:03 UTC


README

Travis CI Status

目录

介绍

PHP CLI 开发通常与 web 开发处理方式大不相同。然而,从更高(和抽象)的角度来看,它们的工作方式相当相似

  1. 用户给出输入
  2. 应用程序执行某些操作
  3. 将响应返回给用户

鉴于第 1.3. 点在 web 和 CLI 开发之间差异很大,第 2. 点,即应用程序的业务逻辑,理论上不应关心输入和输出的形式,因此构建 web 或 CLI 应用程序之间不应有任何差异。

此包的主要目标是提供一个方法,为 CLI 做出我们多年来为 web 做出的:解耦上述 3 个 步骤 中的任何一个,而不依赖于强制特定(且有限)应用程序结构的庞大包。

一种新方法:CLI 路由器

多年来,我们使用“路由器”作为将用户输入映射到应用程序逻辑的方式来进行 web 开发。为什么我们不能为 CLI 做同样的事情呢?

SeaLion 正是这样:一个将 CLI 输入(参数、选项等)映射到 某个东西 的路由器。这个 某个东西 是什么,或者应该是什么,取决于库的用户。

通过这种方式,可以编写完全由任何库或框架解耦的 CLI 应用程序。

一些术语

在 SeaLion 中,CLI 的输入由以下内容定义

  • 命令 总是 CLI 输入中提供的第一个参数(文件路径之后)。每个 CLI 输入只有一个 命令

  • 参数 是命令的 位置参数。实际上,只能通过位置来区分一个参数与另一个参数,而不能通过名称。

  • 选项 是命令的 命名参数。它们通过在名称前加双横线 -- 传递给输入。

传递值是可选的(省略时默认为 true)。

要传递值,必须跟在选项名称后面的是等号和值本身:--foo=bar

也可以用引号包裹值:--foo="bar"。实际上,传递包含空格的值时需要这样做:--foo='bar bar bar'

  • 标志选项 的工作方式完全相同,但它们通过在名称前加单个横线 - 传递给输入,例如:-bar="baz!"

“标志”/“选项”名称用于替代 PHP “选项”/“长选项” 名称,因为我发现后者非常令人困惑:实际上并不是选项“长”,而是字面量。

例如,在 CLI 中输入以下文本

php app.php greet good morning -to="Giuseppe" --yell

SeaLion 将识别

  • 'greet' 作为命令
  • 'good''morning' 作为两个参数
  • 'to' 作为带有值为 'Giuseppe' 的标志
  • 'yell' 作为带有值为 true 的选项

请注意,输入顺序仅对命令有效,命令必须是文件名之后的第一个参数,对于命令之后的任何内容,顺序都不重要,以 -- 开头的任何内容都是选项,以 - 开头的任何内容是标志,其余的所有内容都是参数。

期望

就像HTTP请求的网关一样,SeaLion通过设置用户输入的预期来工作。可以在命令(必需)、参数、选项和标志上设置预期。

可以通过以下方式设置参数、选项和标志的预期

  • 精确匹配:为了验证预期,输入中的参数必须与所需字符串完全相等
  • 正则表达式匹配:为了验证预期,输入中的参数必须与给定的正则表达式匹配
  • 布尔匹配:为了验证预期,参数必须是true或false。注意,没有值的选项或标志被认为是true,而未提供的参数被认为是false(因此,将预期设置为 true 使参数成为必需)
  • 回调匹配:输入中提供的值传递给给定的可调用对象,预期验证回调是否返回true。

只能通过精确匹配设置命令的预期。

回调匹配当然是最灵活的选项,允许执行任何操作,例如,您可能接受并验证控制台的JSON输入。

添加期望

大多数情况下,您只需要与 Toobo\SeaLion\Router 交互。

预期通过 Router::addCommand() 方法添加,该方法返回一个 Toobo\SeaLion\Route\Route 类的实例,可以在该实例上调用以下方法

  • withArguments()
  • withOptions()
  • withFlags()

分别添加对参数、选项和标志的预期。

简单示例

class_alias('Toobo\SeaLion\Router', 'Router');

$router = new Router();

$router->addCommand('greet', 'handler0')
    ->withArguments([0 => 'R{/^g[\w]+/i}', 1 => true])
    ->withFlags(['to' => 'Giuseppe']);
    ->withOptions(['yell' => function($yell) {
        return in_array($yell, ['', true, false], true);
    }]);

上述预期将在以下情况下得到满足

  • 第一个参数(键 0)与正则表达式 /^g[\w]+/i 匹配,因为SeaLion使用 "R{$regex}" 语法添加正则表达式预期
  • 第二个参数(键 1)已提供并且非空
  • 标志 to 将正好是 "Giuseppe"
  • 选项 yell 将是空字符串、true或false。选项(或标志)
    • true 当没有值时传递: --yell
    • false 当根本未提供时
    • 一个空字符串 当设置为这样: --yell='' 或当省略值,但提供了等号: --yell=

例如,以下输入验证了上述所有预期

php app.php greet --yell Good Morning -to=Giuseppe

处理器

我把命令和参数预期的组合称为 "Route"。当所有预期都得到满足时,我说 "a route matched"。

如果有多个路由匹配,则只返回第一个(按添加顺序)。

那么,当路由匹配时会发生什么?

路由响应的一部分将是 handler,可以是任何东西。

在上面的例子中,handler是字符串 "handler0",但它可以是可调用对象、数组、对象...

如何实现应用程序流程留给应用程序。

SeaLion只是一个将一些CLI输入映射到一些输出的路由器:输出应该是什么以及如何使用它超出了SeaLion的范围。

验证期望

要解析添加的路由并获取匹配的路由,只需要 执行 路由器。事实上,路由器对象是一个 functor,即它有一个 __invoke() 方法,允许像调用回调一样调用它。

$result = $router();

上面的 $result 变量将包含一个包含4个元素的数组

  • 第一个元素是 truefalse,如果路由器分别找到了匹配的路由或未找到。
  • 第二个元素是
    • 如果匹配了路由,则为 handler。实际上,handler可以是任何东西
    • 如果没有匹配路由,它是一个 二进制标志的位掩码,提供有关为什么没有找到匹配项的信息
  • 第三个元素是匹配的命令或如果没有匹配则返回false。注意,即使没有路由匹配,命令也可能匹配,因为这可能与参数期望有关。
  • 第四个元素是用户输入的所有输入参数的数组,其中
    • 键为Router::ARGUMENTS的元素是所有输入中使用的参数的数组
    • 键为Router::OPTIONS的元素是所有输入中使用的选项的数组
    • 键为Router::FLAGS的元素是所有输入中使用的标志的数组

完整使用示例

这是一个SeaLion的简单但完整的用法示例

require 'vendor/autoload.php';

use Toobo\SeaLion\Router;

/**
 * An helper function to output some text in the console
 **/
function writeLine($text) {
  $f = fopen('php://stdout', 'w');
  fwrite($f, $text.PHP_EOL);
  fclose($f);
}

// In this trivial example we just have an array of callbacks
// where the one to execute is choosed based on the route
$handlers = [
    'handler0' => function($command, array $input) {
        writeLine('Command executed: '.$command);
        writeLine('Arguments used: '.json_encode($input[Router::ARGUMENTS]));
        writeLine('Options used: '.json_encode($input[Router::OPTIONS]));
        writeLine('Flags used: '.json_encode($input[Router::FLAGS]));
    },
    'handler1' => function($command, $input) {
        // do something interesting
    },
    'error' => function($errorBitmask, $command) {
        writeLine('Something gone wrong.');
        // let's use bitmask of error constants to output error message
        if ($command === false) {
            writeLine('No or invalid command was used.');
        } else {
            writeLine('The command '.$command.' was not used properly:');
            if ($errorBitmask & Router::ARGS_NOT_MATCHED) {
                writeLine('Arguments used were not valid.');
            }
            if ($errorBitmask & Router::OPTIONS_NOT_MATCHED) {
                writeLine('Options used were not valid.');
            }
            if ($errorBitmask & Router::FLAGS_NOT_MATCHED) {
                writeLine('Options used were not valid.');
            }
        }
    }
];

$router = new Router();

// add some commands and respective handlers
$router->addCommand('com1', 'handler0')->withArguments([true]); // 1st arg is required
$router->addCommand('com2', 'handler1');

$routeInfo = $router(); // execute the router

if ($routeInfo[0]) { // $routeInfo[0] is true when a route matched
    $handler = $routeInfo[1];
    call_user_func($handlers[$handler], $routeInfo[2], $routeInfo[3]);
} else {
    call_user_func($handlers['error'], $routeInfo[1], $routeInfo[2]);
}

假设上面的代码保存在一个名为app.php的文件中,在控制台运行

php app.php com1 Hello! --test -a -b --foo="bar"

控制台输出的将是

Command executed: com1
Arguments used: ["Hello!"]
Options used: {"test":true,"foo":"bar"}
Flags used: {"a":true,"b":true}

相反,使用

php app.php com1 --test -a -b --foo="bar"

控制台输出的将是

Something gone wrong.
The command com1 was not used properly:
Arguments used were not valid.

因为第一个参数是必需的,但没有提供。

更好的输出

如何使用SeaLion提供的信息取决于使用它的应用程序。然而,你很可能想将一些文本输出到控制台作为响应。

前面示例中使用的超级简单的fwrite就是这样做的,但是,可能能够格式化输出,例如使用一些颜色。

这超出了SeaLion的范畴,但是没有任何东西阻止你使用Composer来使用任何能够做到这一点的库,当然,你也可以编写自己的代码来实现这一点。

当然,Symfony控制台组件可能是一个选项,但如果存在其他选项,例如由Maxime Bouroumeau-Fuseau创建的轻量级且易于使用的ConsoleKit

假设你已经通过Composer安装了它,使用它来输出带颜色的消息非常简单,例如

use ConsoleKit\Colors;

//...

$handlers = [
    'handler0' => function($command, array $input) {
        writeLine(
            Colors::cyan("Command executed: ")
            .Colors::colorize($command, Colors::GREEN|Colors::BOLD)
        );
        writeLine(
            Colors::magenta("Arguments used: ")
            .Colors::yellow(json_encode($input[Router::ARGUMENTS]))
        );
        writeLine(
            Colors::cyan("Options used: ")
            .Colors::green(json_encode($input[Router::OPTIONS]))
        );
        writeLine(
            Colors::magenta("Flags used: ")
            .Colors::yellow(json_encode($input[Router::FLAGS]))
        );
    },
    
    //...
]

预览

Console colors preview

同一命令上的多个路由

在上面的示例中,每个命令通常只有一个路由。这不是一个规则,实际上,在相同的命令上使用不同的参数期望是有可能有多条路由的。

$router->addCommand('com1', 'handler0')->withFlags(['choose' => 'A']);
$router->addCommand('com1', 'handler1')->withFlags(['choose' => 'B']);
$router->addCommand('com1', 'handler2')->withFlags(['choose' => 'C']);

使用上面的代码,为'com1'命令有3条路由。

当使用将标志choose设置为'A'时,第一个路由匹配,并返回的处理程序是'handler0'。当相同的标志的值为'B'时,第二个路由匹配(并返回的处理程序是'handler1'),最后,当标志的值为'C'时,第三条路由匹配。

输入类

出于任何原因,例如测试,可能希望模拟控制台输入以由SeaLion路由器解析。

这可以通过实现Toobo\SeaLion\Input\InputInterface接口的对象来完成。

SeaLion提供了2个这样的对象

  • Toobo\SeaLion\Input\ArgvInput
  • Toobo\SeaLion\Input\StringInput

第一个接受以$_SERVER['argv']格式的参数数组,第二个接受作为字符串的输入。

路由器构造函数接受作为第一个参数的输入对象实例,当提供时,它用于模拟控制台输入。

示例

use Toobo\SeaLion\Router;
use Toobo\SeaLion\Input\StringInput;

$input = new StringInput('greet Good Morning --yell -name="Giuseppe"');
$router = new Router($input);

可以像上面一样使用ArgvInput类获得相同的结果

use Toobo\SeaLion\Router;
use Toobo\SeaLion\Input\ArgvInput;

$input = new ArgvInput(['greet', 'Good', 'Morning', '--yell', '-name="Giuseppe"']);
$router = new Router($input);

你可以通过扩展Toobo\SeaLion\Input\InputInterface接口来编写自定义的输入对象。

自定义分发器

如上所述,当路由器执行时,它返回一个数组,其中包含有关匹配的路由或没有匹配的路由的原因的信息。

然而,这是默认行为。实际上,SeaLion使用一个类:Toobo\SeaLion\Dispatcher\Dispatcher来返回这些结果。总是有可能通过扩展Toobo\SeaLion\Dispatcher\DispatcherInterface接口来编写自定义的调度器类。

这是一个非常简单的接口,只有两个方法:success()error()

实现这两个方法可以使你能够自定义SeaLion在路由匹配和未匹配时的行为。

例如,你可能希望在未匹配路由时抛出异常或运行默认例程;或者你可能只想接受特定类型的处理程序,例如立即执行的回调。

这完全取决于你。

要使用自定义分发器,您需要将其实例作为第二个参数传递给路由构造函数。

一旦第一个参数用于自定义输入类,如果您想覆盖默认的分发器但不想覆盖输入,则需要将第一个参数设置为null,例如:

$dispatcher = new MyCustomDispatcher();
$router = new Router(null, $dispatcher);

需求

  • PHP 5.4+
  • 使用Composer进行安装

安装

SeaLion是一个在Packagist上可用的Composer包,可以通过运行以下命令进行安装:

composer require toobo/sealion:~0.1

单元测试

SeaLion仓库包含一些针对PHPUnit编写的单元测试。

要运行测试,请从控制台导航到仓库文件夹并运行

phpunit

许可证

SeaLion采用MIT许可证发布,有关更多信息,请参阅LICENSE文件。