「Artisan Command 不是應該寫成通用、可重複執行的工具嗎?」
大部分教學會教你把 Command 設計成通用工具——吃參數、可以重複跑、要考慮各種邊界情況。但真實系統裡,也有另一種完全合理的 Command 用途:一次性地修正一批資料,跑完這一次,這支 Command 的任務就結束了。今天要看的就是這種案例。
這個系統從舊系統遷移資料的過程裡,發現一批新聞資料缺少了某個欄位的值——這個欄位在遷移當下沒有被正確帶過來。修法是寫一支專門處理這個缺失的 Command:
class FixNewsGraph extends Command
{
protected $signature = 'fix:news-graph';
protected $description = 'Command description';
public function handle(): int
{
Utils::csvReader(resource_path('seeders/graph.csv'))->each(function (array $row) {
if (empty($row['apply_no'])) {
return;
}
when(
News::query()->where('foreign_id', '=', $row['apply_no'])->first(),
static fn (News $news) => $news->fill(['graph' => $row['graph']])->save()
);
});
News::query()
->where('content', 'LIKE', '%class="news_image"%')
->each(fn (News $news) => $news->touch());
return self::SUCCESS;
}
}
邏輯很直接:讀一份 CSV,逐行用 foreign_id(對應舊系統的識別碼)去查對應的新聞資料,找到就補上缺失的欄位值,找不到就直接跳過(when() 是一個輔助函式,第一個參數是 null 或 falsy 時,第二個 callback 就不會被執行)。
如果照著通用工具的標準來看,這支 Command 有很多「不夠嚴謹」的地方——沒有 --dry-run 選項先看看會影響哪些資料、找不到對應資料時只是安靜跳過,沒有留下任何記錄、$description 甚至還留著預設值沒改。但這些「不足」其實是刻意的:這支 Command 的任務範圍非常明確,就是「把這批已知的資料缺失修正一次」,跑完之後不會再被呼叫第二次。 幫一支只會執行一次的工具做完整的錯誤處理、記錄、互動確認,投入的心力跟它實際的使用壽命不成比例。
注意最後一段:
News::query()
->where('content', 'LIKE', '%class="news_image"%')
->each(fn (News $news) => $news->touch());
這段在做的事,是把「內容裡含有特定 CSS class 的圖片」的新聞資料全部 touch() 一次——touch() 會更新這筆資料的 updated_at 時間戳,但不改變任何實際內容。這通常是為了觸發某個依賴「資料被更新」這件事的機制(例如快取失效、或某個監聽 updated 事件的 Observer 要重新處理內容)。這種「順手在同一支修復 Command 裡處理相關的副作用」,是真實維運工作常見的模式——不是每個問題都值得開一支獨立的 Command,把相關聯的修正放在同一次執行裡,反而更貼近實際發生問題的脈絡。
這支 Command 之所以存在,背後的脈絡是這個系統曾經從一套舊系統把歷史資料整批遷移過來。從舊系統匯入資料時,欄位對不齊、某些衍生欄位在舊系統裡沒有直接對應、遷移腳本漏算了某個欄位——這些狀況幾乎是資料遷移的常態,不是例外。 與其在遷移當下想辦法把每一種邊界情況都處理到完美,更務實的做法往往是:遷移完成後,用類似這支 Command 的方式,針對事後發現的具體缺失,各自寫一支小工具補救。
class FixNewsGraph extends Command
{
protected $signature = 'fix:news-graph
{--dry-run : 只顯示會影響的資料,不實際執行}
{--chunk=100 : 每批處理筆數}';
// 加上完整的進度條、記錄、錯誤處理...
}
如果這支 Command 真的只會執行一次,投入這些設計換來的維護價值有限。
public function handle(): int
{
// 讀取資料、逐筆修正,找不到就跳過
return self::SUCCESS;
}
(Day 28 會看到另一支資料遷移 Command,設計成完全相反的風格——因為那支要處理的是全站規模的路徑搬遷,風險等級不一樣,值得投入 --dry-run/分批處理/完整統計,兩者的差異正好可以對照著看。)
你的專案裡有沒有為了處理一次性的資料問題,寫過一支「夠用就好」的 Command?回頭看的話,那個「夠用就好」的判斷,事後證明是對的嗎?
FixNewsGraph 讀一份 CSV,逐筆比對舊系統識別碼、補上缺失欄位,找不到就跳過touch() 觸發依賴更新時間的機制),貼近實際問題發生的脈絡明天要看排程機制——這個系統怎麼設計 sitemap 自動產生,而且刻意把「產生」跟「回應請求」拆成兩件事。