资讯专栏INFORMATION COLUMN

Laravel 动态添加 Artisan 命令的最佳实践

ninefive / 961人阅读

摘要:初步尝试既然最常见的注册命令的方式是修改类中的,那么一般正常人都会从这边开始下手。又要自己取出实例,又要自己调用方法,调用方法之前还有自己先把实例化这么繁琐,肯定不是运行时添加命令的最佳实践,所以我决定继续寻找更优解。

本文首发于我的博客,原文链接:https://blessing.studio/best-...

虽然 Laravel 官方文档提供的添加 Artisan Command 的方法是直接修改 app/Console/Kernel.php 文件并在 $commands 属性中注册要添加的 Artisan 命名的类名(Laravel 服务容器会自动解析),但是,如果我们出现需要「动态(运行时)添加 Artisan 命令」的需求的话,就会很容易吃瘪。因为,Laravel 的文档(当然,我说的是官网上的)几乎没有提到任何关于这方面的内容。

这也是我为什么总是吐槽 Laravel 文档有些地方很烂的原因 —— 很多时候你为了实现一个文档里没提到的功能,需要去翻半天 Laravel 的框架源码才能找到解决方法(我博客的 Laravel 标签 下已经有不少这样的踩坑文了)。虽然 Laravel 框架的源码很优雅,看着也不会难受,但是在一堆文件中跳来跳去寻找逻辑浪费脑细胞的行为还是能省则省吧 :(

这次要实现的功能是在运行时动态加载自定义的 Artisan Command(更详细一些的需求就是在皮肤站的一个插件中注册 Artisan 命令,Laravel 插件系统的实现可以参考我之前的 另一篇文章)。

TL;DR 太长不看

总之先上干货,毕竟不是所有人都喜欢听我废话一大堆后才拿到解决方案的。

Laravel 5.3 及以上:

Artisan::starting(function ($artisan) {
    // 传入类名字符串即可,会被服务容器自动解析
    $artisan->resolve("ExampleFooCommand");
    // 批量添加
    $artisan->resolveCommands([
        "ExampleFuckCommand",
        "ExampleShitCommand"
    ]);
    // 参数必须为 SymfonyComponentConsoleCommandCommand 的实例
    // 继承自 IlluminateConsoleCommand 的类实例也可以
    $artisan->add($command);
});

Laravel 5.2:

Event::listen("IlluminateConsoleEventsArtisanStarting", function ($event) {
    // 其他用法同上
    $event->artisan->resolve("ExampleBarCommand");
});

Laravel 5.1:

Event::listen("artisan.start", function ($event) {
    // 其他用法同上
    $event->artisan->resolve("ExampleWtfCommand");
});

接下来就是我摸索时尝试的步骤,写下来权当记录水博文,发了发牢骚,有兴趣的就继续看下去吧。

0x01 初步尝试

既然 Laravel 最常见的注册 Artisan 命令的方式是修改 APPConsoleKernel 类中的 $commands,那么一般正常人都会从这边开始下手。可以看到,这个类是继承自 IlluminateFoundationConsoleKernel 类并覆写了 $commands 属性。让我们稍微看一下这个 $commands 属性用在哪了:

/**
 * Get the Artisan application instance.
 *
 * @return IlluminateConsoleApplication
 */
protected function getArtisan()
{
    if (is_null($this->artisan)) {
        return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                            ->resolveCommands($this->commands);
    }

    return $this->artisan;
}

可以看到,这个方法用单例模式实例化了一个 Artisan(ArtisanIlluminateConsoleApplication 的别名),其中最重要的是调用了 IlluminateConsoleApplication::resolveCommands 这个方法,并且将那个注册了自定义 Artisan 命令的属性给传了进去。我们跳转到那个 resolveCommands 方法看一看……

/**
 * Add a command, resolving through the application.
 *
 * @param  string  $command
 * @return SymfonyComponentConsoleCommandCommand
 */
public function resolve($command)
{
    return $this->add($this->laravel->make($command));
}

/**
 * Resolve an array of commands through the application.
 *
 * @param  array|mixed  $commands
 * @return $this
 */
public function resolveCommands($commands)
{
    $commands = is_array($commands) ? $commands : func_get_args();

    foreach ($commands as $command) {
        $this->resolve($command);
    }

    return $this;
}

代码条理很清晰,挨个儿把那些 $commands 中的元素给丢进 Laravel 服务容器里实例化之后,调用父类方法 SymfonyComponentConsoleApplication::add (是的,Laravel 用了很多很多 Symfony 的组件)添加到自身实例中,持引用以供之后的调用所需。

继续翻看 IlluminateFoundationConsoleKernel 的源码,可以看到 Laravel 贴心地开放了一个 registerCommand 方法:

/**
 * Register the given command with the console application.
 *
 * @param  SymfonyComponentConsoleCommandCommand  $command
 * @return void
 */
public function registerCommand($command)
{
    $this->getArtisan()->add($command);
}

那么我们要做的就是,在运行时中拿到 Kernel 的实例,并且通过调用 registerCommand 方法把我们的自定义 Artisan 命令也给加进去。那么我们要怎样才能拿到这个实例呢?

相信对 Laravel 有所了解的各位都会想到 —— 服务容器。

通过查阅 Laravel 命令行入口(根目录下的 artisan 文件)源码可以知道,Laravel 就是使用服务容器来实例化 Kernel 的:

$kernel = $app->make(IlluminateContractsConsoleKernel::class);

如果你有心的话,会发现 Laravel 框架的 Web 入口文件(public/index.php)和命令行入口文件中实例化 Kernel 的语句都是一样的,那么为什么通过 Web 访问时解析出来的是 AppHttpKernel 的实例而通过命令行访问时解析出来的就是 AppConsoleKernel 的实例了呢?

这里就涉及 Laravel 服务容器的一个强大的核心功能 —— 绑定接口至实现。因为这些实例都实现了相同的接口,所以我们可以使用相同的代码并且很方便地更换接口后的具体实现,这也是使用 IoC 容器的好处之一,有兴趣的多去了解了解吧 :)

闲话休提,那么我们只要通过服务容器就可以拿到 Kernel 实例了(当然,如果你愿意,你也可以直接通过 $GLOBAL["kernel"] 来访问全局作用域下定义的那个 $kernel 变量,效果都是一样的,但是太 tmd lowb 了,所以我不愿意用),看起来已经离成功了一大半呢!

$kernel = app("IlluminateContractsConsoleKernel");
// 因为 registerCommand 方法只接受 SymfonyComponentConsoleCommandCommand 的实例作为参数
$kernel->registerCommand(app("ExampleFooCommand"));

然后我们执行一下 php artisan list,就能看到我们的命令已经出现啦:

Laravel Framework version 5.2.45

Usage:
  command [options] [arguments]

Available commands:
  help           Displays help for a command
  list           Lists commands
  foo            Example command

但是等等……Laravel 自带的那些 makemigrate 等命令哪里去了?我最开始出现这个问题的时候还以为是我太早把 Kernel 解析出来了,后来直接使用 $GLOBALS["kernel"] 也是一样的问题时才认识到问题另有原因。仔细阅读源码后发现 Artisan 命令行在调用(handlecall 等方法)之前都会调用这样一个方法:

$this->bootstrap();

通过阅读源码可以知道这个 bootstrap 方法就是用来加载 Laravel 框架的基本组件的,包括 IlluminateFoundationProvidersArtisanServiceProvider 这个服务提供者中提供的所有框架内置 Artisan 命令。好在这个方法是 public 的,所以我们只要在 registerCommand 之前调用一下这个方法就可以啦:

$kernel = app("IlluminateContractsConsoleKernel");
$kernel->bootstrap();
$kernel->registerCommand(app("ExampleFooCommand"));

如果你愿意,你甚至还可以直接使用 Artisan 这个 Facade,因为它就是指向 IlluminateContractsConsoleKernel 的:

Artisan::bootstrap();
Artisan::registerCommand(app("InsaneProfileCacheCommandsClean"));

结果如下:

0x02 继续尝试

虽然这样确实能够实现我们的需求,但是我觉得这样不行(话说我都不晓得嘻哈梗怎么突然就流行起来了,虽然确实蛮有意思的啦)。

又要自己取出 Kernel 实例,又要自己调用 bootstrap 方法,调用 registerCommand 方法之前还有自己先把 Command 实例化……这么繁琐,肯定不是运行时添加 Artisan 命令的最佳实践,所以我决定继续寻找更优解。

虽然我们上面用的方法是取出 Kernel 实例并进行操作的,但是其实该方法里的操作也是基于 getArtisan 所获取的 IlluminateConsoleApplication (?这玩意在 Laravel 源码里经常被 as 为 Artisan)实例进行的。可惜的是这个方法是 protected 的,我们无法直接调用它,所以我们还是先去看这个类的源码吧:

/**
 * Create a new Artisan console application.
 *
 * @param  IlluminateContractsContainerContainer  $laravel
 * @param  IlluminateContractsEventsDispatcher  $events
 * @param  string  $version
 * @return void
 */
public function __construct(Container $laravel, Dispatcher $events, $version)
{
    parent::__construct("Laravel Framework", $version);

    $this->laravel = $laravel;
    $this->setAutoExit(false);
    $this->setCatchExceptions(false);

    $events->fire(new EventsArtisanStarting($this));
}

瞧我发现了什么?Artisan 在实例化之后会触发一个 IlluminateConsoleEventsArtisanStarting 事件,并且把自身实例给传递过去。那么我们要做的就很简单了:监听该事件,拿到 Artisan 实例,调用 resolveresolveCommands 方法来注册我们的 Artisan 命令即可。

具体的方法在最上面给出了,我这里就不多说了。另外需要注意的是,Laravel 5.1 版本并没有 ArtisanStarting 这个事件,而是 artisan.start,不过原理都是一样的:

$events->fire("artisan.start", [$this]);

另外,在 Laravel 5.3 及以上版本中,Artisan 还贴心地提供了 Artisan::starting 这个方法,和监听事件的效果差不多,不过是直接修改实例的 $bootstrappers 属性的,传递一个闭包进去即可,示例代码见最上方。

0x03 一些牢骚

虽然只要看源码就能知道,Laravel 框架很多地方都预留了非常多的接口,让我们可以方便优雅地实现很多自定义功能,这也是我为什么喜欢这个框架的原因之一。

但是……但是,你的文档就不能写好一点吗!哪怕提一下这些 API 也好啊!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/23337.html

相关文章

  • Laravel 5 程序优化技巧

    摘要:使用即时编译器和都能轻轻松松的让你的应用程序在不用做任何修改的情况下,直接提高或者更高的性能,之前做个一个实验,具体请见使用提升程序性能。 本文经授权转自 PHPHub 社区 说明 性能一直是 Laravel 框架为人诟病的一个点,所以调优 Laravel 程序算是一个必学的技能。 接下来分享一些开发的最佳实践,还有调优技巧,大家有别的建议也欢迎留言讨论。 这里是简单的列表: 配置信...

    habren 评论0 收藏0
  • 分享一些简单 Laravel 编码实践

    摘要:关于,它使用起来简单且舒适适用于编写产品代码,并能极大的推动开发过程。这里有一些在开发中值得记住的简单建议最大限度的使用你的文件不要破坏框架核心,不要编辑文件夹中的文件,你可以选择继承相关函数来实现。 showImg(https://segmentfault.com/img/remote/1460000018416776?w=808&h=449); 将任何 PHP 框架称为最好的框架都...

    cyixlq 评论0 收藏0
  • Laravel 编码实践分享

    摘要:关于,它使用起来简单且舒适适用于编写产品代码,并能极大的推动开发过程。中我最喜欢的一点是它是使用当下编程中的最佳实践所构建的。的工作原理是这样的,对于一个命名为的表,希望该表的模型被命名为。尽量为每一个请求创建。 showImg(https://segmentfault.com/img/remote/1460000018303541?w=808&h=449); 将任何 PHP 框架称为...

    wean 评论0 收藏0
  • Laravel入门及实践,快速上手ThinkSNS+二次开发

    摘要:在中,提示符可能是。框架使用来执行安装及管理依赖。为了能访问网页,要启动程序服务器。在大多数类系统中,包括,命令行提示符是符号。这两个操作分别对应于的和,即创建和读取。首个表单要在模板中编写表单,可以使用表单构造器。 【摘要】自从ThinkSNS+不使用ThinkPHP框架而使用Laravel框架之后,很多人都说技术门槛抬高了,其实你与TS+的距离仅仅只是学习一个新框架而已,所以,我们...

    glumes 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<