解决办法中用到的router和cliApp都是在bean处理器初始化时生成的bean对象.
public function handle(): bool{ if (!$this->application->beforeConsole()) { return false; } // 获取路由bean对象 /** @var Router $router */ $router = bean('cliRouter'); // 注册路由对象 // Register console routes CommandRegister::register($router); // 打印注册信息 CLog::info('Console command route registered (group %d, command %d)', $router->groupCount(), $router->count()); // 启动控制台app,接下来框架的工作交给控制台app接管 // Run console application if ($this->application->isStartConsole()) { bean('cliApp')->run(); } // 完结控制台处理器 return $this->application->afterConsole();}
尽管路由对象和控制台app对象都是在bean处理器中初始化的,然而本章节还是有必要梳理一下这两个应用到的bean对象的初始化流程.因为bean处理器初始化bean的时候,如果bean对象有init办法,会调用init办法.所以,这两个对象的初始化咱们次要关怀两个办法:__constract和init.
先看router对象Swoft\Console\Router\Router
:
因为此类没有上述两种办法,且bean配置中没有结构参数的配置,所以能够确定此对象只是单纯的new了进去放在了对象池中.没有其它的初始化动作.
再看cliApp对象Swoft\Console\Application
:
public function __construct(array $options = []){ // 因为bean配置中没有这个bean的结构参数,所以此处的options是空数组 // 这个办法也等于没有做其它任何操作 ObjectHelper::init($this, $options);}
因为没有init办法,所以cliApp的初始化简直也是什么也没干!
显然,咱们在控制台输出的命令只能是在cliApp对象的run办法中得以调用了.
接下来,咱们来看cliApp的run办法:
public function run(): void{ // 与httpServer组件的设计很相似 // 用户的业务逻辑被try/catch包裹 // 如果产生谬误,则交给错误处理调度者来解决 try { // 预处理,筹备run办法须要用到的货色 // Prepare for run $this->prepare(); // 触发ConsoleEvent::RUN_BEFORE事件 Swoft::trigger(ConsoleEvent::RUN_BEFORE, $this); // Get input command // 通过input对象获行保留的取执命令 if (!$inputCommand = $this->input->getCommand()) { $this->filterSpecialOption(); } else { // 执行命令 $this->doRun($inputCommand); } Swoft::trigger(ConsoleEvent::RUN_AFTER, $this, $inputCommand); } catch (Throwable $e) { /** @var ConsoleErrorDispatcher $errDispatcher */ $errDispatcher = BeanFactory::getSingleton(ConsoleErrorDispatcher::class); // Handle request error $errDispatcher->run($e); }}
预处理办法:
protected function prepare(): void{ // 将input和output对象赋值给以后对象 // 这里须要看input和output的初始化过程 $this->input = Swoft::getBean('input'); $this->output = Swoft::getBean('output'); // load builtin comments vars // 将input对象上的例如pwd等信息与以后对象的commentsVars数组进行合并 $this->setCommentsVars($this->commentsVars());}
Swoft\Console\Input\Input
的bean定义是@Bean("input")
,所以者是一个单例的bean对象,再看构造方法:
public function __construct(array $args = null, bool $parsing = true){ // 因为没有input的bean结构参数定义,所以此处$args肯定是null if (null === $args) { // 将超全局数组$_SERVER中保留的命令行参数赋值给$args $args = (array)$_SERVER['argv']; } // 将参数保留在以后对象的tokens属性上 $this->tokens = $args; // 参数的第一个值是执行的启动脚本 $this->scriptFile = array_shift($args); // 这里获取的是除去启动脚本后的残余参数 $this->fullScript = implode(' ', $args); // find command name, other is flags // 从残余的参数中寻找并设置此次须要解决的命令并设置在以后对象上,并返回去掉命令后残余的参数数组 // 再将残余的参数保留在以后对象的flags属性上 $this->flags = $this->findCommand($args); // 获取以后应用程序的执行目录 // 也就是用户执行swoft脚本时所处的目录 $this->pwd = $this->getPwd(); if ($parsing) { // list($this->args, $this->sOpts, $this->lOpts) = InputParser::fromArgv($args); // 调用toolkit/cli-utils包,将命令行参数解析成对应的参数、短选项和长选项,有趣味能够应用一下这个包 // 这个包还有给命令行输入设置色彩的性能 [$this->args, $this->sOpts, $this->lOpts] = Flags::parseArgv($this->flags); }}
设置命令办法:
protected function findCommand(array $flags): array{ if (!isset($flags[0])) { return $flags; } // Not input command name if (strpos($flags[0], '-') === 0) { return $flags; } $this->command = trim($flags[0]); // remove first element, reset index key. unset($flags[0]); return array_values($flags);}
总之,通过input构造函数解决后,咱们曾经将入口文件、程序启动目录、执行命令、执行参数和执行长短选项都保留在了input对象上.
接下来看Swoft\Console\Output\Output
对象.bean注解为@Bean("output")
,可见也是单例对象.构造方法如下:
public function __construct($outputStream = null){ // 因为没有定义输入流,所以此处仍旧是默认的STDOUT if ($outputStream) { $this->outputStream = $outputStream; } // 初始化控制台输入款式对象. $this->getStyle();}
执行命令办法:
protected function doRun(string $inputCmd): void{ // 获取控制台输入对象 $output = $this->output; /* @var Router $router */ // 获取router对象 $router = Swoft::getBean('cliRouter'); // 匹配命令 $result = $router->match($inputCmd); // Command not found // 如果未匹配到后果 if ($result[0] === Router::NOT_FOUND) { // 获取所有的命令名称 $names = $router->getAllNames(); // 控制台打印命令不存在谬误 $output->liteError("The entered command '{$inputCmd}' is not exists!"); // find similar command names by similar_text() // 通过similar_text()找到相似的命令 if ($similar = Arr::findSimilar($inputCmd, $names)) { // 控制台打印相似命令的揭示 $output->writef("nMaybe what you mean is:n <info>%s</info>", implode(', ', $similar)); } else { // 打印控制台利用帮忙信息 $this->showApplicationHelp(false); } return; } // 获取匹配到的路由信息 $info = $result[1]; // 获取组名 $group = $info['group']; // 将组名设置到commentsVars数组 $this->addCommentsVar('groupName', $group); // 如果只输出了组名称 则显示组的帮忙信息 // Only input a group name, display help for the group if ($result[0] === Router::ONLY_GROUP) { // Has error command if ($cmd = $info['cmd']) { $output->error("Command '{$cmd}' is not exist in group: {$group}"); } $this->showGroupHelp($group); return; } // 如果输出中蕴含了帮忙选项 则显示命令的帮忙信息 // Display help for a command if ($this->input->getSameOpt(['h', 'help'])) { $this->showCommandHelp($info); return; } // 解析默认选项和参数 // Parse default options and arguments // 依据路由中的选项,再次解析input中残余的args选项 $this->input->parseFlags($info, true); // 设置命令的ID $this->input->setCommandId($info['cmdId']); // 触发ConsoleEvent::DISPATCH_BEFORE事件 Swoft::triggerByArray(ConsoleEvent::DISPATCH_BEFORE, $this, $info); // Call command handler /** @var ConsoleDispatcher $dispatcher */ // 获取ConsoleDispatcher的bean对象 $dispatcher = Swoft::getSingleton('cliDispatcher'); // 执行调度 $dispatcher->dispatch($info); // 触发ConsoleEvent::DISPATCH_AFTER事件 Swoft::triggerByArray(ConsoleEvent::DISPATCH_AFTER, $this, $info);}
调度办法:
public function dispatch(array $route): void{ // Handler info // 从路由信息中获取handler的类名和办法 [$className, $method] = $route['handler']; // Bind method params // 获取须要传递给办法的参数数组 $params = $this->getBindParams($className, $method); // 获取handler类的bean对象 $object = Swoft::getSingleton($className); // Blocking running // 如果不是协程执行 if (!$route['coroutine']) { // 调用执行前办法,外部是触发了ConsoleEvent::EXECUTE_BEFORE事件 $this->before($method, $className); // 调用handler的执行办法并传入构建好的参数 $object->$method(...$params); // 调用执行后办法,外部是触发了ConsoleEvent::EXECUTE_AFTER事件 $this->after($method); // 执行完结 return; } // 如果是协程执行 // Hook php io function // 开启一键协程化 Runtime::enableCoroutine(); // 如果是处于单元测试环境 // If in unit test env, has been in coroutine. if (defined('PHPUNIT_COMPOSER_INSTALL')) { $this->executeByCo($object, $method, $params); return; } // 否则开启协程执行 // Coroutine running srun(function () use ($object, $method, $params) { $this->executeByCo($object, $method, $params); });}
private function getBindParams(string $class, string $method): array{ // 获取类的反射构造 $classInfo = Swoft::getReflection($class); // 如果类信息外面没有调用的办法信息 则返回空数组 if (!isset($classInfo['methods'][$method])) { return []; } // 保留参数的数组 // binding params $bindParams = []; // 获取办法的参数数组 $methodParams = $classInfo['methods'][$method]['params']; /** * @var string $name * @var ReflectionType $paramType * @var mixed $devVal */ // 遍历参数列表,获取参数类型和默认值 foreach ($methodParams as [, $paramType, $devVal]) { // 获取参数类型的名称 // Defined type of the param $type = $paramType->getName(); // 将对应的input或output对象保留进参数数组,其它类型参数保留为null if ($type === Output::class) { $bindParams[] = Swoft::getBean('output'); } elseif ($type === Input::class) { $bindParams[] = Swoft::getBean('input'); } else { $bindParams[] = null; } } return $bindParams;}
类反射信息池数据结构:
/** * Reflection information pool * * @var array * * @example * [ * 'className' => [ //类名 * 'comments' => 'class doc comments', //类正文 * 'methods' => [ //办法数组 * 'methodName' => [ //办法名 * 'params' => [ //参数数组 * 'argName', // like `name` 参数名 * 'argType', // like `int` 参数类型 * null // like `$arg` 默认值 * ], * 'comments' => 'method doc comments', //办法正文 * 'returnType' => 'returnType/null' //返回值类型 * ] * ] * ] * ] */
协程执行handler类:
public function executeByCo($object, string $method, array $bindParams): void{ try { // 创立控制台协程上下文 Context::set($ctx = ConsoleContext::new()); // 触发before事件 $this->before($method, get_class($object)); // 执行handler的解决办法 $object->$method(...$bindParams); // 触发after事件 $this->after($method); } catch (Throwable $e) { /** @var ConsoleErrorDispatcher $errDispatcher */ // 如果执行出错则由谬误处理器接管 $errDispatcher = Swoft::getSingleton(ConsoleErrorDispatcher::class); // Handle request error $errDispatcher->run($e); } finally { // 触发协程生命周期事件 // Defer Swoft::trigger(SwoftEvent::COROUTINE_DEFER); // Complete Swoft::trigger(SwoftEvent::COROUTINE_COMPLETE); }}
总结:
1.控制台处理器其实非常简单,获取了router和cliApp,绑定路由并执行cliApp的run办法.2.run办法的流程: (1).预处理,将控制台参数等数据处理好后分门别类保留. (2).匹配路由,获取命令对应的hanler类和办法. (3).按路由信息中返回的是否用协程形式执行,应用对应的阻塞执行或协程执行形式,调用解决类bean对象的对应办法.