源码分析laravel artisan命令行

在使用laravel进行开发,我们会经常用到laravel的命令行php artisan去执行一些操作,如生成文件,启动队列,执行数据库迁移等。这让我有点好奇artisan的工作原理是怎样的。所以想通过阅读源码尝试分析其工作原理,当然如果有错误的地方,欢迎指出。

在laravel项目的一级目录会有artisan.php文件,这是执行php artisan的入口文件,代码非常简洁,就只有几行代码就可以让我们实现命令行的功能。

//声明php脚本,跟shell脚本中的#!/bin/bash声明类似
#!/usr/bin/env php

<?php

//引入自动加载机制
require __DIR__.'/bootstrap/autoload.php';

//引入laravel容器核心
$app = require_once __DIR__.'/bootstrap/app.php';

//实例化控制台核心类
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

//执行php artisan的命令
$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

//结束命令行进程
$kernel->terminate($input, $status);

exit($status);

其中$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);实例化的核心类是在./bootstrap/app.php中通过下面代码绑定的单例对象。完成绑定后可通过make()方法将绑定的对象通过反射类的形式实例化。

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

其中./app/Console/Kenel.php控制台核心类继承了Illuminate\Foundation\Console\Kernel类,这个继承的类为运行php artisan的核心类,我们查看Kernel类中的handle方法。

//其中$input,$output分别为Symfony\Component\Console\Input\ArgvInput类和Symfony\Component\Console\Output\ConsoleOutput分别用于处理参数的输入和结果的输出
public function handle($input, $output = null)
    {
        try {
            /**
            *将$bootstrappers数组中的定义的类绑定到容器中,并执行bootstrap()初始化类
            *包括初始化加载env,加载config配置,加载错误处理机制,注册门面类,加载服务提供者等
            **/
            $this->bootstrap();
            //获取artisan实例并执行run()方法
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            //异常捕获
            $this->reportException($e);
            $this->renderException($output, $e);

            return 1;
        }
    }

查看artisan实例的run(),实例通过use Illuminate\Console\Application as Artisan;引入;其中$exitCode = parent::run($input, $output);为执行的核心,通过继承Symfony\Component\Console\Application类以实现其核心功能

    /**
     * {@inheritdoc}
     */
    public function run(InputInterface $input = null, OutputInterface $output = null)
    {
        //获取命令,如执行php artisan make:controller AdminController,则获取make:controller命令
        $commandName = $this->getCommandName(
            $input = $input ?: new ArgvInput
        );
        //添加CommandStarting分发事件
        $this->events->dispatch(
            new CommandStarting(
                $commandName, $input, $output = $output ?: new ConsoleOutput
            )
        );
        //执行命令
        $exitCode = parent::run($input, $output);
        //添加CommandFinished分发事件
        $this->events->dispatch(
            new CommandFinished($commandName, $input, $output, $exitCode)
        );

        return $exitCode;
    }

查看Symfony\Component\Console\Application类中的run()方法,这里简单说明一下这个run(),其执行的核心如下

...代码省略...
//基于用户参数和选项配置输入和输出实例。
$this->configureIO($input, $output);

try {
    //执行命令
    $exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
    //异常捕获,对不存在的命令或执行出错的命令进行捕获处理输出
    if (!$this->catchExceptions) {
        throw $e;
    }
    //执行定义的闭包函数
    $renderException($e);
    ...代码省略...
}

$renderException($e);
...代码省略...

再来看看doRun()方法的执行

public function doRun(InputInterface $input, OutputInterface $output)
    {
        //检查是否包含--version或-V两个参数,若存在则无论则直接返回laravel的版本号
        if (true === $input->hasParameterOption(['--version', '-V'], true)) {
            $output->writeln($this->getLongVersion());

            return 0;
        }

        try {
            //绑定命令的基本参数,如执行命令php artisan make:migration create_users_table --create=users对其中的参数进行解析绑定
            $input->bind($this->getDefinition());
        } catch (ExceptionInterface $e) {
            //异常捕获,使程序即使报错也能正常执行
        }
        //获取命令名称,若执行php artisan make:migration create_users_table --create=users命令则获取make:migration
        $name = $this->getCommandName($input);
        //判断是否包含--help或-h参数
        if (true === $input->hasParameterOption(['--help', '-h'], true)) {
            if (!$name) {
                //若命令为空,php artisan等价于php artisan help
                $name = 'help';
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
            } else {
                $this->wantHelps = true;
            }
        }
        //判断命令为空,为其设置相关参数
        if (!$name) {
            $name = $this->defaultCommand;
            $definition = $this->getDefinition();
            $definition->setArguments(array_merge(
                $definition->getArguments(),
                [
                    'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
                ]
            ));
        }

        try {
            $this->runningCommand = null;
            //获取命令具体详情,相当于一个工厂函数
            $command = $this->find($name);
        } catch (\Throwable $e) {
            //对异常命令或错误命令进行处理
            if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
                ...代码省略...
        }

        $this->runningCommand = $command;
        //再次执行命令
        $exitCode = $this->doRunCommand($command, $input, $output);
        $this->runningCommand = null;

        return $exitCode;
    }

这里查看工厂函数find()方法

public function find(string $name)
    {
        //初始化命令,相当于为$this->commands赋值所有命令类,其key值为命令名称,value为具体命令类实例
        $this->init();

        $aliases = [];
        //遍历$this->commands将命令别名也添加到$this->commands变量中
        foreach ($this->commands as $command) {
            //dump('命令名称:'.$command->getName().PHP_EOL);
            //dump('类名称:'.get_class($command));
            foreach ($command->getAliases() as $alias) {
                if (!$this->has($alias)) {
                    $this->commands[$alias] = $command;
                }
            }
        }
        //判断命令是否存在
        if ($this->has($name)) {
            //命令存在则返回当前命令的命令类
            return $this->get($name);
        }
        ...代码省略...
    }

这里执行命令,dump打印输出一下相关信息,如下所示

源码分析laravel artisan命令行

往下查看doRunCommand()方法,当程序能够进入到这个函数,说明当前的命令是存在的。再查看其代码流程如下

//$command为具体的命令类
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
    {
        //设置一些不清楚是啥的helper,应该是命令帮助设置吧
        foreach ($command->getHelperSet() as $helper) {
            if ($helper instanceof InputAwareInterface) {
                $helper->setInput($input);
            }
        }
        if (null === $this->dispatcher) {
            /**
            **具体核心是执行$command->run()
            **以php artisan cache:clear为例,这里实例的命令类为Illuminate\Cache\Console\ClearCommand,每一个命令类其底层都继承了Symfony\Component\Console\Command\Command这个基础命令类,这里执行的run()就是由这里继承而已,但最终执行每个命令类具体操作是执行具体命令类中的handle()方法
            **/
            return $command->run($input, $output);
        }
        ...代码省略...
    }

以上就是我对于laravel artisan命令行的源码分析。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1

正好前段时间,苦恼于 artisan 不能用继承,如果两个命令有共同代码,只能用 helper 的方式引入,于是仿照 artisan 新写了一套命令行代码。新脚本能够实现以下功能,方便平时开发上线工作:

如有aaa 和 bbb 等多个项目,且各有 dev 和 online 等环境,则建立目录及文件 aaa/user/init_user.php。

写好 init_user脚本后按 php _artisan aaa.user.init:user.dev –run 来执行即可。

这种方式能够让我将多个项目的脚本,都放到此处统一管理,且内部自动配置好数据库连接,仅需执行 aaa.user.init:user.online 即可在线上执行

3年前 评论
oliver-l (楼主) 3年前
LuminEe (作者) 3年前
oliver-l (楼主) 3年前
Complicated 3年前
Complicated 3年前
LuminEe (作者) 3年前
Complicated 3年前
LuminEe (作者) 3年前
Complicated 3年前
Complicated 3年前
Complicated 3年前
LuminEe (作者) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!