乐趣区

关于php:从binswoft开始阅读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 对象的对应办法.
退出移动版