[譯]Laravel 5.0 之命令及處理程式
本文譯自 Matt Stauffer 的系列文章.
本文中涉及的新功能都是關於 Commands 的,這些特性在 Laravel 舊版本中已經有了,但是在 Laravel 5.0 中變得更加好用了。
本文中將會用到例子來自於我正在開發的一個叫做 SaveMyProposals 的新應用。
什麼是 Command, Command handler 和 Command bus?
在 Shawn McCool 的這篇文章 中,你可以深入瞭解 command, command handler, command bus 的概念。但總的來說:
Command 是一個代表資訊的簡單物件。它只包含你打算做某件事時需要用到的資訊。在我們接下來的例子中,它就是 "複製談話命令(Duplicate Talk Command)", 任何時候當用戶要複製一條談話建議時,我們的系統就會呼叫它。 這個 "重複談話命令" 會包含複製一個談話所需要的全部屬性集——比如一個序列化的 Talk 物件或者是 TaldId.
Command Handler 則是用於對 Command 做出響應的一個類。Command 可以在一個或多個 Handlers 之間傳遞, 每個 Handler 從 Command 中取出重要的資訊並做某些操作來響應。
Command bus 是一套用於排程 Commands 的系統。它把 commands 與對應的 Handlers 進行匹配,並使它們能夠一起工作。一般情況下,人們需要編寫自己的 command bus, 但 Laravel 內建了一個開箱即用的 Command bus, 所以至少在本文涉及的範圍內我們不用擔心這個問題。
在 Laravel 中使用 Command
在開始介紹 Laravel 5.0 中使用 Command 的整個結構之前,我們先看看最終的用例是怎樣的。假設一個使用者訪問了系統的某個路由,比如 savemyproposals.com/talks/12345/duplicate
TalkController@duplicate(12345)
.
下面是處理這個請求的路由和方法:
// HttpControllersTalkController ... public function duplicate($talkId) { $talk = Talk::findOrFail($talkId); $this->dispatch(new DuplicateTalkCommand($talk)); // 取決於具體的實現,這兩行程式碼也可能簡化為一行程式碼: // $this->dispatch(new DuplicateTalkCommand($talkId)); }
接下來是 Command 的程式碼:
// CommandsDuplicateTalkCommand
...
class DuplicateTalkCommand extends Command
{
public $talk;
public function __construct(Talk $talk)
{
$this->talk = $talk;
}
}
然後是 Command handler:
// HandlersCommandsDuplicateTalkCommandHandler
...
class DuplicateTalkCommandHandler
{
public function handle(DuplicateTalkCommand $command)
{
// 對 $command 變數進行某些操作
dd($command);
}
}
就如上面的程式碼所展示的,控制器通過一些必要的資訊建立了一個 DuplicateTalkCommand
物件,通過內建的 command bus dispatcher 對齊進行排程,於是該命令的處理程式自動對其進行處理。
體系結構
接下來,我們先來看看這些命令和處理程式存放在什麼位置,然後再說說如何生成它們。
資料夾
在 Laravel 5.0 的應用框架中,app/
目錄下有兩個新的資料夾:Commands
和 Handlers
, Handlers
目錄下還有兩個子目錄:Commands
和 Events
(這個目錄說明我們還可以期待事件處理的特性).
app/
Commands/
Handlers/
Commands/
Events/
相信看到目錄結構你就能猜到,Commands 的程式碼存放在 app/Commands
目錄下,而 Command handlers 則存放在 app/Handlers/Commands
目錄下—— Handler 的檔名與其對應的 Command 保持一致,但是要加上 Handler 字尾。
Artisan
非常值得慶幸的是,你不用自己動手來建立 Command 和 Command Handler。新版本提供了一個全新的 Artisan 生成工具,通過它可以快速生成這些檔案:
$ php artisan make:command DuplicateTalkCommand
預設情況下,這條命令會生成一個自處理的命令(不生成單獨的 Command handler),並且該命令不新增到佇列。加上 --handler
引數可以同時生成 handler, 加上 --queued
引數可以將其加入到佇列。
執行這個 artisan 命令會生成兩個檔案: 命令檔案(app/Commands/DuplicateTalkCommand.php
) 和 處理程式檔案(app/Handlers/Commands/DuplicateTalkCommandHandler.php
) (假設使用了 --handler
引數),並且生成的處理程式中的 handle
方法會自動加上與其匹配的命令的型別約束。
基本工作流程
綜上所述,要建立一個新的 DuplicateTalkCommand
, 你需要執行以下工作:
php artisan make:command DuplicateTalkCommand
- 編輯
DuplicateTalkCommand
, 增加一個 public 屬性$talk
並在建構函式中初始化這個屬性。 - 編輯
DuplicateTalkCommandHandler
, 在handle()
方法中編寫具體程式碼,完成你需要執行的操作。 - 在控制器或者 Artisan 命令中排程(呼叫)這個命令。
佇列
把命令加入佇列
如果希望某個命令在每次被呼叫時加入到佇列中以便非同步執行,你需要做的是讓該命令實現 ShouldBeQueued
介面。 Laravel 會發現這個介面並把其加入佇列等候執行,而不是立即執行。
...
class DuplicateTalkCommand extends Command implements ShouldBeQueued
{
//...
}
InteractsWithQueue trait
在你的 Command 類中加上這個 trait, 會讓你的 Command 具有在以前版本中用慣了的佇列命令(queue commands)所具有的全部特性:$command->release()
, $command->delete()
, $command->attempts()
等等。
...
class DuplicateTalkCommand extends Command implements ShouldBeQueued, InteractsWithQueue
{
//...
}
SerializesModels trait
如果你傳入一個 Eloquent 模型作為屬性,就像前面的例子中那樣,並且希望命令放入佇列中執行而不是同步執行,那麼必須要考慮到 Eloquent 模型的序列化,這可能會給你帶來一些麻煩。不過在 Laravel 5.0 版本中,你可以給你的 Command 加一個 名為 SerializesModels
的 trait 來解決這個問題。方法很簡單,在類的程式碼塊頂部加上即可:
...
class DuplicateTalkCommand extends Command implements ShouldBeQueued
{
use SerializesModels;
// ...
}
Dispatcher
DispatchesCommands Trait
你可能注意到,在前面的例子中,我們可以直接在控制器中使用 $this->dispatch()
方法。這是控制器的一個語法糖。這個語法糖實際上是通過名為 DispathesCommands
的 trait 來實現的。你可以在控制器之外的任何地方使用這個 trait.
比如,你希望某個服務類可以在方法中使用 $this->dispatch()
, 你只要在你的服務類的程式碼塊頂部使用 DispatchesCommands
這個 trait 即可:
...
class MyServiceClass {
use DispatchesCommands;
//...
}
注入 bus
如果你希望更直接、更清楚地呼叫 Command bus 而不是藉助於 Laravel 系統提供的 trait, 你可以直接向你的類的建構函式或者是方法注入 IlluminateContractsBusDispatcher
例項(參見 Laravel 5.0 之方法注入)。
...
public function __construct(IlluminateContractsBusDispatcher $bus)
{
$this->bus = $bus;
}
public function doSomething()
{
$this->bus->dispatch(new Command);
}
dispatchFrom(command::class, $request 或任何 arrayAccessible 物件)
在之前的例子中,我們已經看到了呼叫命令的最簡單方式,就是 $bus->dispatch(new Command(params...))
. 但有時候由於新建命令的引數列表變得越來越大——比如,當你的命令用於來處理表單輸入的時候:
...
class CreateTalkCommand extends Command
{
public function __construct($title, $description, $outline, $organizer_notes, $length, $type, $level) {
// ...
}
}
這時,如果還用之前的方式來例項化命令,程式碼會變得很難看:
$this->dispatch(new CreateTalkCommand($input['title'], $input['description'],
$input['outline'], $input['organizer_notes'], $input['length'],
$input['type'], $input['level']));
通常我們的表單請求會傳遞與屬性相同 key 的陣列,從陣列或者請求物件中獲得具體的值。幸虧 Laravel 5.0 有針對這種情況的解決方案:
$this->dispatch('NameOfCommand', $objectThatImplementsPHPArrayAccessible);
因此我們可以用下面的程式碼替換上面那長長的一串:
$this->dispatchFrom(CreateTalkCommand::class, $input);
或者這樣:
public function doSomethingInController(Request $request)
{
$this->dispatchFrom(CreateTalkCommand::class, $request);
// ...
}
Laravel 會自動在傳入的陣列或者 arrayAccessible
物件中去尋找與屬性名相同的 key, 取出對應的值來呼叫命令的建構函式。
自處理命令
如果你嫌每個命令都要建立一個 Command 類和一個 Command handler 類太麻煩的話,你可以建立一個“自處理”的 Command. 這種情況下 Command 只有單一的處理程式,且該處理程式就是 Command 自己。要讓一個 Command 變成自處理,只需要給該 command 類加一個 handle()
方法,並讓它實現 SelfHandling
介面:
...
class DuplicateTalkCommand extends Command implements SelfHandling
{
//...
public function handle()
{
//...
}
其它注意事項
- 命令處理程式會由 IOC 容器進行解析,因此你可以注入 repositories, service classes 或者任何其它型別到你的命令處理程式的建構函式中,然後在
handle()
方法中呼叫它們。 - 幾乎所有相關的 trait 和 介面都在
IlluminateContractsBus
或者IlluminateContractsQueue
名稱空間下。比如,IlluminateContractsBusSelfHandling
. - 如果你的命令是放入佇列執行,不需要在處理程式的最後執行
$command->delete()
方法。只要你的處理程式沒有丟擲任何異常,Laravel 會假定它已經正確完成,並自動將其從佇列中移除。
寫在最後
就這麼多了,如果我遺漏了什麼,或者某個問題講得不夠清楚,請讓我知道。本文涉及到的點還有一些需要補充和替換的地方。暫時來說,我希望本文可以幫助你瞭解新版 Laravel 中的 Command 的執行機制。此外,Taylor 在 Laracasts 上的視訊 涵蓋了本文的全部內容並且講得更多。