toobo / sealion
命令行界面(CLI)路由器。
Requires (Dev)
- phpunit/phpunit: ~4.4
This package is auto-updated.
Last update: 2024-08-29 03:58:03 UTC
README
目录
介绍
PHP CLI 开发通常与 web 开发处理方式大不相同。然而,从更高(和抽象)的角度来看,它们的工作方式相当相似
- 用户给出输入
- 应用程序执行某些操作
- 将响应返回给用户
鉴于第 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=
- 是 true 当没有值时传递:
例如,以下输入验证了上述所有预期
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个元素的数组
- 第一个元素是
true
或false
,如果路由器分别找到了匹配的路由或未找到。 - 第二个元素是
- 如果匹配了路由,则为 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])) ); }, //... ]
预览
同一命令上的多个路由
在上面的示例中,每个命令通常只有一个路由。这不是一个规则,实际上,在相同的命令上使用不同的参数期望是有可能有多条路由的。
$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文件。