摘要:初步嘗試既然最常見的注冊(cè)命令的方式是修改類中的,那么一般正常人都會(huì)從這邊開始下手。又要自己取出實(shí)例,又要自己調(diào)用方法,調(diào)用方法之前還有自己先把實(shí)例化這么繁瑣,肯定不是運(yùn)行時(shí)添加命令的最佳實(shí)踐,所以我決定繼續(xù)尋找更優(yōu)解。
本文首發(fā)于我的博客,原文鏈接:https://blessing.studio/best-...
雖然 Laravel 官方文檔提供的添加 Artisan Command 的方法是直接修改 app/Console/Kernel.php 文件并在 $commands 屬性中注冊(cè)要添加的 Artisan 命名的類名(Laravel 服務(wù)容器會(huì)自動(dòng)解析),但是,如果我們出現(xiàn)需要「動(dòng)態(tài)(運(yùn)行時(shí))添加 Artisan 命令」的需求的話,就會(huì)很容易吃癟。因?yàn)?,Laravel 的文檔(當(dāng)然,我說的是官網(wǎng)上的)幾乎沒有提到任何關(guān)于這方面的內(nèi)容。
這也是我為什么總是吐槽 Laravel 文檔有些地方很爛的原因 —— 很多時(shí)候你為了實(shí)現(xiàn)一個(gè)文檔里沒提到的功能,需要去翻半天 Laravel 的框架源碼才能找到解決方法(我博客的 Laravel 標(biāo)簽 下已經(jīng)有不少這樣的踩坑文了)。雖然 Laravel 框架的源碼很優(yōu)雅,看著也不會(huì)難受,但是在一堆文件中跳來跳去尋找邏輯浪費(fèi)腦細(xì)胞的行為還是能省則省吧 :(
這次要實(shí)現(xiàn)的功能是在運(yùn)行時(shí)動(dòng)態(tài)加載自定義的 Artisan Command(更詳細(xì)一些的需求就是在皮膚站的一個(gè)插件中注冊(cè) Artisan 命令,Laravel 插件系統(tǒng)的實(shí)現(xiàn)可以參考我之前的 另一篇文章)。
TL;DR 太長(zhǎng)不看總之先上干貨,畢竟不是所有人都喜歡聽我廢話一大堆后才拿到解決方案的。
Laravel 5.3 及以上:
Artisan::starting(function ($artisan) { // 傳入類名字符串即可,會(huì)被服務(wù)容器自動(dòng)解析 $artisan->resolve("ExampleFooCommand"); // 批量添加 $artisan->resolveCommands([ "ExampleFuckCommand", "ExampleShitCommand" ]); // 參數(shù)必須為 SymfonyComponentConsoleCommandCommand 的實(shí)例 // 繼承自 IlluminateConsoleCommand 的類實(shí)例也可以 $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"); });
接下來就是我摸索時(shí)嘗試的步驟,寫下來權(quán)當(dāng)記錄水博文,發(fā)了發(fā)牢騷,有興趣的就繼續(xù)看下去吧。
既然 Laravel 最常見的注冊(cè) Artisan 命令的方式是修改 APPConsoleKernel 類中的 $commands,那么一般正常人都會(huì)從這邊開始下手??梢钥吹?,這個(gè)類是繼承自 IlluminateFoundationConsoleKernel 類并覆寫了 $commands 屬性。讓我們稍微看一下這個(gè) $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; }
可以看到,這個(gè)方法用單例模式實(shí)例化了一個(gè) Artisan(Artisan 是 IlluminateConsoleApplication 的別名),其中最重要的是調(diào)用了 IlluminateConsoleApplication::resolveCommands 這個(gè)方法,并且將那個(gè)注冊(cè)了自定義 Artisan 命令的屬性給傳了進(jìn)去。我們跳轉(zhuǎn)到那個(gè) 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; }
代碼條理很清晰,挨個(gè)兒把那些 $commands 中的元素給丟進(jìn) Laravel 服務(wù)容器里實(shí)例化之后,調(diào)用父類方法 SymfonyComponentConsoleApplication::add (是的,Laravel 用了很多很多 Symfony 的組件)添加到自身實(shí)例中,持引用以供之后的調(diào)用所需。
繼續(xù)翻看 IlluminateFoundationConsoleKernel 的源碼,可以看到 Laravel 貼心地開放了一個(gè) registerCommand 方法:
/** * Register the given command with the console application. * * @param SymfonyComponentConsoleCommandCommand $command * @return void */ public function registerCommand($command) { $this->getArtisan()->add($command); }
那么我們要做的就是,在運(yùn)行時(shí)中拿到 Kernel 的實(shí)例,并且通過調(diào)用 registerCommand 方法把我們的自定義 Artisan 命令也給加進(jìn)去。那么我們要怎樣才能拿到這個(gè)實(shí)例呢?
相信對(duì) Laravel 有所了解的各位都會(huì)想到 —— 服務(wù)容器。
通過查閱 Laravel 命令行入口(根目錄下的 artisan 文件)源碼可以知道,Laravel 就是使用服務(wù)容器來實(shí)例化 Kernel 的:
$kernel = $app->make(IlluminateContractsConsoleKernel::class);
如果你有心的話,會(huì)發(fā)現(xiàn) Laravel 框架的 Web 入口文件(public/index.php)和命令行入口文件中實(shí)例化 Kernel 的語句都是一樣的,那么為什么通過 Web 訪問時(shí)解析出來的是 AppHttpKernel 的實(shí)例而通過命令行訪問時(shí)解析出來的就是 AppConsoleKernel 的實(shí)例了呢?
這里就涉及 Laravel 服務(wù)容器的一個(gè)強(qiáng)大的核心功能 —— 綁定接口至實(shí)現(xiàn)。因?yàn)檫@些實(shí)例都實(shí)現(xiàn)了相同的接口,所以我們可以使用相同的代碼并且很方便地更換接口后的具體實(shí)現(xiàn),這也是使用 IoC 容器的好處之一,有興趣的多去了解了解吧 :)
閑話休提,那么我們只要通過服務(wù)容器就可以拿到 Kernel 實(shí)例了(當(dāng)然,如果你愿意,你也可以直接通過 $GLOBAL["kernel"] 來訪問全局作用域下定義的那個(gè) $kernel 變量,效果都是一樣的,但是太 tmd lowb 了,所以我不愿意用),看起來已經(jīng)離成功了一大半呢!
$kernel = app("IlluminateContractsConsoleKernel"); // 因?yàn)?registerCommand 方法只接受 SymfonyComponentConsoleCommandCommand 的實(shí)例作為參數(shù) $kernel->registerCommand(app("ExampleFooCommand"));
然后我們執(zhí)行一下 php artisan list,就能看到我們的命令已經(jīng)出現(xiàn)啦:
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 自帶的那些 make、migrate 等命令哪里去了?我最開始出現(xiàn)這個(gè)問題的時(shí)候還以為是我太早把 Kernel 解析出來了,后來直接使用 $GLOBALS["kernel"] 也是一樣的問題時(shí)才認(rèn)識(shí)到問題另有原因。仔細(xì)閱讀源碼后發(fā)現(xiàn) Artisan 命令行在調(diào)用(handle、call 等方法)之前都會(huì)調(diào)用這樣一個(gè)方法:
$this->bootstrap();
通過閱讀源碼可以知道這個(gè) bootstrap 方法就是用來加載 Laravel 框架的基本組件的,包括 IlluminateFoundationProvidersArtisanServiceProvider 這個(gè)服務(wù)提供者中提供的所有框架內(nèi)置 Artisan 命令。好在這個(gè)方法是 public 的,所以我們只要在 registerCommand 之前調(diào)用一下這個(gè)方法就可以啦:
$kernel = app("IlluminateContractsConsoleKernel"); $kernel->bootstrap(); $kernel->registerCommand(app("ExampleFooCommand"));
如果你愿意,你甚至還可以直接使用 Artisan 這個(gè) Facade,因?yàn)樗褪侵赶?IlluminateContractsConsoleKernel 的:
Artisan::bootstrap(); Artisan::registerCommand(app("InsaneProfileCacheCommandsClean"));
結(jié)果如下:
0x02 繼續(xù)嘗試雖然這樣確實(shí)能夠?qū)崿F(xiàn)我們的需求,但是我覺得這樣不行(話說我都不曉得嘻哈梗怎么突然就流行起來了,雖然確實(shí)蠻有意思的啦)。
又要自己取出 Kernel 實(shí)例,又要自己調(diào)用 bootstrap 方法,調(diào)用 registerCommand 方法之前還有自己先把 Command 實(shí)例化……這么繁瑣,肯定不是運(yùn)行時(shí)添加 Artisan 命令的最佳實(shí)踐,所以我決定繼續(xù)尋找更優(yōu)解。
雖然我們上面用的方法是取出 Kernel 實(shí)例并進(jìn)行操作的,但是其實(shí)該方法里的操作也是基于 getArtisan 所獲取的 IlluminateConsoleApplication (?這玩意在 Laravel 源碼里經(jīng)常被 as 為 Artisan)實(shí)例進(jìn)行的。可惜的是這個(gè)方法是 protected 的,我們無法直接調(diào)用它,所以我們還是先去看這個(gè)類的源碼吧:
/** * 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)); }
瞧我發(fā)現(xiàn)了什么?Artisan 在實(shí)例化之后會(huì)觸發(fā)一個(gè) IlluminateConsoleEventsArtisanStarting 事件,并且把自身實(shí)例給傳遞過去。那么我們要做的就很簡(jiǎn)單了:監(jiān)聽該事件,拿到 Artisan 實(shí)例,調(diào)用 resolve 或 resolveCommands 方法來注冊(cè)我們的 Artisan 命令即可。
具體的方法在最上面給出了,我這里就不多說了。另外需要注意的是,Laravel 5.1 版本并沒有 ArtisanStarting 這個(gè)事件,而是 artisan.start,不過原理都是一樣的:
$events->fire("artisan.start", [$this]);
另外,在 Laravel 5.3 及以上版本中,Artisan 還貼心地提供了 Artisan::starting 這個(gè)方法,和監(jiān)聽事件的效果差不多,不過是直接修改實(shí)例的 $bootstrappers 屬性的,傳遞一個(gè)閉包進(jìn)去即可,示例代碼見最上方。
0x03 一些牢騷雖然只要看源碼就能知道,Laravel 框架很多地方都預(yù)留了非常多的接口,讓我們可以方便優(yōu)雅地實(shí)現(xiàn)很多自定義功能,這也是我為什么喜歡這個(gè)框架的原因之一。
但是……但是,你的文檔就不能寫好一點(diǎn)嗎!哪怕提一下這些 API 也好??!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/23337.html
摘要:使用即時(shí)編譯器和都能輕輕松松的讓你的應(yīng)用程序在不用做任何修改的情況下,直接提高或者更高的性能,之前做個(gè)一個(gè)實(shí)驗(yàn),具體請(qǐng)見使用提升程序性能。 本文經(jīng)授權(quán)轉(zhuǎn)自 PHPHub 社區(qū) 說明 性能一直是 Laravel 框架為人詬病的一個(gè)點(diǎn),所以調(diào)優(yōu) Laravel 程序算是一個(gè)必學(xué)的技能。 接下來分享一些開發(fā)的最佳實(shí)踐,還有調(diào)優(yōu)技巧,大家有別的建議也歡迎留言討論。 這里是簡(jiǎn)單的列表: 配置信...
摘要:關(guān)于,它使用起來簡(jiǎn)單且舒適適用于編寫產(chǎn)品代碼,并能極大的推動(dòng)開發(fā)過程。這里有一些在開發(fā)中值得記住的簡(jiǎn)單建議最大限度的使用你的文件不要破壞框架核心,不要編輯文件夾中的文件,你可以選擇繼承相關(guān)函數(shù)來實(shí)現(xiàn)。 showImg(https://segmentfault.com/img/remote/1460000018416776?w=808&h=449); 將任何 PHP 框架稱為最好的框架都...
摘要:關(guān)于,它使用起來簡(jiǎn)單且舒適適用于編寫產(chǎn)品代碼,并能極大的推動(dòng)開發(fā)過程。中我最喜歡的一點(diǎn)是它是使用當(dāng)下編程中的最佳實(shí)踐所構(gòu)建的。的工作原理是這樣的,對(duì)于一個(gè)命名為的表,希望該表的模型被命名為。盡量為每一個(gè)請(qǐng)求創(chuàng)建。 showImg(https://segmentfault.com/img/remote/1460000018303541?w=808&h=449); 將任何 PHP 框架稱為...
摘要:在中,提示符可能是??蚣苁褂脕韴?zhí)行安裝及管理依賴。為了能訪問網(wǎng)頁,要啟動(dòng)程序服務(wù)器。在大多數(shù)類系統(tǒng)中,包括,命令行提示符是符號(hào)。這兩個(gè)操作分別對(duì)應(yīng)于的和,即創(chuàng)建和讀取。首個(gè)表單要在模板中編寫表單,可以使用表單構(gòu)造器。 【摘要】自從ThinkSNS+不使用ThinkPHP框架而使用Laravel框架之后,很多人都說技術(shù)門檻抬高了,其實(shí)你與TS+的距離僅僅只是學(xué)習(xí)一個(gè)新框架而已,所以,我們...
閱讀 3031·2021-11-12 10:36
閱讀 4772·2021-09-22 10:57
閱讀 1584·2021-09-22 10:53
閱讀 2673·2019-08-30 15:55
閱讀 3504·2019-08-29 17:00
閱讀 3362·2019-08-29 16:36
閱讀 2478·2019-08-29 13:46
閱讀 1356·2019-08-26 11:45