从bin/swoft开始,阅读Swoft框架源码(八)--ConsoleProsser控制台处理器

马尔科夫尼可夫

处理方法中用到的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对象的对应方法.
阅读 264

酷白发,小酒窝,主角标配的帅小伙~

7 声望
2 粉丝
0 条评论
你知道吗?

酷白发,小酒窝,主角标配的帅小伙~

7 声望
2 粉丝
宣传栏