Starting from bin / swoft, read the source code of swoft framework (8) — console Prosser console processor

Time:2021-3-8

Router and cliapp are bean objects generated during initialization of bean processor

public function handle(): bool
{
     if (!$this->application->beforeConsole()) {
        return false;
     }
     //Get routing bean object
     /** @var Router $router */
     $router = bean('cliRouter');
     //Register routing objects
     // Register console routes
     CommandRegister::register($router);
     //Print registration information
     CLog::info('Console command route registered (group %d, command %d)', $router->groupCount(), $router->count());
     //Start the console app, and then the framework will be taken over by the console app
     // Run console application 
     if ($this->application->isStartConsole()) {
        bean('cliApp')->run();
     }
     //End console processor
     return $this->application->afterConsole();
}

Although the routing object and console app object are initialized in the bean processor, it is necessary to sort out the initialization process of these two used bean objects in this chapter. Because when the bean processor initializes the bean, if the bean object has init method, it will call init method. Therefore, we mainly focus on two methods for the initialization of these two objects__ Contract and init

Let’s look at the router object firstSwoft\Console\Router\Router:

Because this class does not have the above two methods, and there is no configuration of construction parameters in bean configuration, it can be determined that this object is simply new and put in the object pool. There is no other initialization action

Another look at cliapp objectsSwoft\Console\Application:

public function __construct(array $options = [])
{
    //Since there is no construction parameter of this bean in the bean configuration, the options here is an empty array
    //This method does not do anything else
    ObjectHelper::init($this, $options);
}
Since there is no init method, the initialization of cliapp does almost nothing!

Obviously, the command we input in the console can only be called in the run method of cliapp object
Next, let’s look at the run method of cliapp

public function run(): void
{
     //It is similar to the design of httpserver component
     //The user's business logic is wrapped by try / catch
     //If an error occurs, it will be handled by the error processing scheduler
     try {
         //Preprocessing, preparing the things needed by the run method
         // Prepare for run
         $this->prepare();
         //Trigger console event:: run_ Before event
         Swoft::trigger(ConsoleEvent::RUN_BEFORE, $this);
         // Get input command
         //Access command saved by input object
         if (!$inputCommand = $this->input->getCommand()) {
            $this->filterSpecialOption();
         } else {
            //Execute the order
            $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);
     }
}

The pretreatment method was as follows

protected function prepare(): void
{
     //Assign input and output objects to the current object
     //Here we need to see the initialization process of input and output
     $this->input = Swoft::getBean('input');
     $this->output = Swoft::getBean('output');
     // load builtin comments vars
     //Merge information such as PWD on the input object with the comments vars array of the current object
     $this->setCommentsVars($this->commentsVars());
}

Swoft\Console\Input\InputThe definition of a bean is@Bean("input")So it is a singleton bean object. Let’s look at the construction method

public function __construct(array $args = null, bool $parsing = true)
{
    //Since there is no bean construction parameter definition for input, here $args must be null
    if (null === $args) {
        //Add super global array$_ The command line parameters saved in server are assigned to $args
        $args = (array)$_SERVER['argv'];
    }
    //Save the parameters on the tokens property of the current object
    $this->tokens = $args;
    //The first value of the parameter is the startup script to execute
    $this->scriptFile = array_shift($args);
    //Here we get the remaining parameters after removing the startup script
    $this->fullScript = implode(' ', $args);

    // find command name, other is flags 
    //Find and set the command to be processed from the remaining parameters, set it on the current object, and return the remaining parameter array after removing the command
    //Then save the remaining parameters in the flags attribute of the current object
    $this->flags = $this->findCommand($args);
    //Gets the execution directory of the current application
    //That is, the directory where the user executes the swoft script
    $this->pwd = $this->getPwd();

    if ($parsing) {
        // list($this->args, $this->sOpts, $this->lOpts) = InputParser::fromArgv($args);
        //Call the toolkit / cli utils package to parse the command line parameters into corresponding parameters, short options and long options. You can use this package if you are interested
        //This package also has the ability to set colors for command line output
        [$this->args, $this->sOpts, $this->lOpts] = Flags::parseArgv($this->flags);
    }
}

Set command method:

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);
}

In a word, after the input constructor processing, we have saved the entry file, program startup directory, execution command, execution parameters and execution length options on the input object

Next lookSwoft\Console\Output\OutputObject. Bean annotated as@Bean("output")It is also a singleton object

public function __construct($outputStream = null)
{
    //Since there is no output stream defined, this is still the default stdout
    if ($outputStream) {
        $this->outputStream = $outputStream;
    }
    //Initializes the console output style object
    $this->getStyle();
}

Command execution method:

protected function doRun(string $inputCmd): void
{
    //Get console output object
    $output = $this->output;
    
    /* @var Router $router */
    //Get router object
    $router = Swoft::getBean('cliRouter');
    //Match command
    $result = $router->match($inputCmd);
    // Command not found
    //If the result is not matched
    if ($result[0] === Router::NOT_FOUND) {
        //Get all command names
        $names = $router->getAllNames();
        //There is no error in the console print command
        $output->liteError("The entered command '{$inputCmd}' is not exists!");
        // find similar command names by similar_text()
        //Through similar_ Text() found a similar command
        if ($similar = Arr::findSimilar($inputCmd, $names)) {
            //The console prints reminders of similar commands
            $output->writef("nMaybe what you mean is:n <info>%s</info>", implode(', ', $similar));
        } else {
            //Print console application help information
            $this->showApplicationHelp(false);
        }
        return;
    }
    //Get the matching routing information
    $info = $result[1];
    //Get group name
    $group = $info['group'];
    //Set the group name to the commentsvars array
    $this->addCommentsVar('groupName', $group);
    //If only the group name is entered, help information for the group is displayed
    // 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; 
    }
    //If the help option is included in the input, the help information of the command is displayed
    // Display help for a command
    if ($this->input->getSameOpt(['h', 'help'])) {
        $this->showCommandHelp($info);
        return; 
    }
    //Resolve default options and parameters
    // Parse default options and arguments
    //According to the options in the route, resolve the remaining args options in the input again
    $this->input->parseFlags($info, true);
    //Set the ID of the command
    $this->input->setCommandId($info['cmdId']);
    //Trigger console event:: Dispatch_ Before event
    
   Swoft::triggerByArray(ConsoleEvent::DISPATCH_BEFORE, $this, $info);
    // Call command handler
    /** @var ConsoleDispatcher $dispatcher */
    //Gets the bean object of the console dispatcher
    $dispatcher = Swoft::getSingleton('cliDispatcher');
    //Execution scheduling
    $dispatcher->dispatch($info);
    //Trigger console event:: Dispatch_ After event
    Swoft::triggerByArray(ConsoleEvent::DISPATCH_AFTER, $this, $info);
}

Scheduling method:

public function dispatch(array $route): void
{
    // Handler info
    //Get the class name and method of handler from routing information
    [$className, $method] = $route['handler'];
    // Bind method params
    //Gets the array of parameters to pass to the method
    $params = $this->getBindParams($className, $method);
    //Gets the bean object of the handler class
    $object = Swoft::getSingleton($className);
    // Blocking running
    //If it's not co-ordinated execution
    if (!$route['coroutine']) {
        //Calling the pre execution method, the console event:: execute is triggered internally_ Before event
        $this->before($method, $className);
        //Call the handler's execution method and pass in the constructed parameters
        $object->$method(...$params);
        //Calling the post execution method, the console event:: execute is triggered internally_ After event
        $this->after($method);
        //End of execution
        return; 
    }
    //If it's a collaborative process implementation
    // Hook php io function
    //Turn on one click programming
    Runtime::enableCoroutine();
    //If you are in a unit test environment
    // If in unit test env, has been in coroutine.
    if (defined('PHPUNIT_COMPOSER_INSTALL')) {
        $this->executeByCo($object, $method, $params);
        return; 
    }
    //Otherwise, start the coroutine execution
    // Coroutine running
    srun(function () use ($object, $method, $params) {
        $this->executeByCo($object, $method, $params);
    });
}
private function getBindParams(string $class, string $method): array
{
    //Gets the reflection structure of the class
    $classInfo = Swoft::getReflection($class);
    //If there is no method information called in the class information, an empty array is returned
    if (!isset($classInfo['methods'][$method])) {
        return [];
    }
    //Save the array of parameters
    // binding params
    $bindParams = [];
    //Gets the parameter array of the method
    $methodParams = $classInfo['methods'][$method]['params'];
    /**
    * @var string         $name
    * @var ReflectionType $paramType
    * @var mixed          $devVal
    */ 
    //Traverse the parameter list to get the parameter type and default value
    foreach ($methodParams as [, $paramType, $devVal]) {
        //Gets the name of the parameter type
        // Defined type of the param
        $type = $paramType->getName();
        //Save the corresponding input or output object into the parameter array, and save other type parameters as null
        if ($type === Output::class) {
            $bindParams[] = Swoft::getBean('output');
        } elseif ($type === Input::class) {
            $bindParams[] = Swoft::getBean('input');
        } else {
            $bindParams[] = null;
        }
    }
    return $bindParams;
}

Class reflection information pool data structure:

/**
 * Reflection information pool * * @var array
 * 
 * @example
 * [
 *'classname' = > [// class name]
 *'comments' = >'class doc comments', // class comments
 *'methods' = > [// method array
 *'methodname '= > [// method name]
 *'params' = > [// parameter array
 *'argname', // like 'name' parameter name
 *'argtype', // like 'Int' parameter type
 *Null // like '$Arg' default value
 *                ], 
 *'comments' = >'method doc comments' // method comments
 *'returntype '= >'returntype / null' // return value type
 *            ] 
 *         ] 
 *     ] 
 * ] 
 */

The handler class is executed by the coroutine

public function executeByCo($object, string $method, array $bindParams): void
{
    try {
        //Create console coordination context
        Context::set($ctx = ConsoleContext::new());
        //Trigger before event
        $this->before($method, get_class($object));
        //How to execute handler
        $object->$method(...$bindParams);
        //Trigger after event
        $this->after($method);
    } catch (Throwable $e) {
        /** @var ConsoleErrorDispatcher $errDispatcher */
        //If the execution fails, it is taken over by the error processor
        $errDispatcher = Swoft::getSingleton(ConsoleErrorDispatcher::class);
        // Handle request error
        $errDispatcher->run($e);
    } finally {
        //Trigger coroutine lifecycle events
        // Defer
        Swoft::trigger(SwoftEvent::COROUTINE_DEFER);
        // Complete
        Swoft::trigger(SwoftEvent::COROUTINE_COMPLETE);
    }
}

Conclusion:

1. The console processor is very simple. It obtains router and cliapp, binds the route and executes the run method of cliapp
2. The flow of run method
    (1) . preprocessing, the console parameters and other data are processed and stored in categories
    (2) Match the route and get the corresponding hanler class and method
    (3) Call the corresponding method to process the bean object according to whether the routing information is executed in the co program mode or not, using the corresponding blocking execution or co program execution mode