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