Laravel 實用小技巧 —— Artisan 入門(下)

快樂的皮拉夫發表於2023-05-20

在上篇文章《 Laravel 實戰小技巧 —— Artisan 入門(上)》 裡,我們介紹了自定義命令建立方式、簽名的定義,這篇文章我們重點為大家介紹命令的輸入輸出相關的內容。

輸入邏輯

介紹完定義命令結構的方式,我們再來看一下命令邏輯的實現部分。

首先是獲取輸入的邏輯。獲取輸入有兩種方式,一種是從命令列中直接獲取輸入,一種是透過命令列互動的方式獲取輸入。

命令列直接輸入

獲取方式 說明
$this->argument('{引數名稱}') 獲取指定引數
$this->arguments() 獲取所有引數
$this->option('{選項名稱}') 獲取指定選項
$this->options() 獲取所有選項

命令列互動輸入

互動方式 說明
$this->ask('{詢問資訊提示}') 以普通輸入形式獲取輸入內容
$this->secret('{密碼資訊提示}') 以密文形式獲取輸入內容
$this->confirm('{確認資訊提示}') 獲取使用者確認資訊輸入,yyes 返回 true,其他輸入返回 false,不區分大小寫
$this->anticipate('{自動補全資訊提示}', ['{提示資訊}']) 獲取使用者輸入內容,並根據使用者輸入提供自動補全資訊提示
$this->choice('{選擇資訊提示}', ['{選項}']) 給使用者提供多個選項,使用者只能輸入合法選項的索引值或選項值,支援多選配置

前三種互動方式比較簡單,我們不做過多介紹,這裡我們再單獨說一下 anticipatechoice 兩個方法。

anticipate 方法

anticipate 這個方法除了提供常規的補全配置外,還提供了閉包的傳參方式,如下:

$name = $this->anticipate('{自動補全提示資訊}', function ($input) {
    // 返回自動完成配置
});

這就比較靈活了,我們可以在閉包中實現我們自定義的補全邏輯。比如當我們需要使用者輸入地區資訊時,就可以在閉包中根據使用者實時輸入的資訊匹配可能的地址資訊,然後作為提示陣列返回給前端。

注意: 閉包中返回的必須是陣列或者實現了 Countable 介面的物件。

choice 方法

choice 方法包含了五個引數,具體描述如下:

$name = $this->choice(
    '{提示資訊}',
    ['{選項1}', '{選項2}'],
    $defaultIndex = null, //預設索引
    $maxAttempts = null, //最大嘗試次數
    $allowMultipleSelections = false //是否支援多選
);
  • defaultIndex: 如果指定了預設索引的話,當使用者在命令列直接按 Enter 鍵提交時,獲取到的就是預設索引對應的值。沒有指定預設索引時,直接 Enter 提交會報錯。
  • maxAttempts: 如果設定了最大嘗試次數的話,當使用者在失敗次數達到 maxAttempts 次以後,會退出互動介面。預設情況下不會退出。
  • allowMultipleSelections: 如果設定了支援多選的話,可以透過輸入選項一,選項二[選項三...] 的方式同時提交多個選項,選項之間透過英文逗號分隔。如果多個選項中存在錯誤的選項,則會丟擲錯誤。

choice 有個特殊的地方,讓我們來看看下面這段程式碼:

$choice = $this->choice('請選擇你的數字', ['2', '1', '0']);

執行命令後,互動介面顯示如下:

Ntv4051SMh.png

比如我們想選擇 0 ,輸入對應的索引 2,這時如果我們在 handle 中獲取 $choice 會不會是我們預期的 0 呢?

並不是。結果輸出的居然是 2 !這是為什麼呢?

我們透過檢視原始碼可以發現,原因就在以下給出的這段程式碼邏輯中(這裡選取重點片段講解):

程式碼: ./vendor/symfony/console/Question/ChoiceQuestion.php

...
$result = array_search($value, $choices); // 查詢選項陣列中是否存在提交的「選項」

// 非關聯陣列處理
if (!$isAssoc) {
    // 存在值的情況返回對應的值
    if (false !== $result) {
        $result = $choices[$result];
    // 不存在對應值的情況判斷是否存在對應的鍵,存在則返回鍵對應的值
    } elseif (isset($choices[$value])) {
        $result = $choices[$value];
    }
//關聯陣列處理
} elseif (false === $result && isset($choices[$value])) {
    // 不存在值的情況下判斷是否存在對應的鍵,如果存在,返回對應的值
    $result = $value;
}
...

從上述程式碼中可以看出,當選項陣列是非關聯陣列時,會先檢查輸入的選項是否是選項陣列的值,如果是的話,則直接將其返回,然後才會檢查是否是選項陣列的鍵。這也就解釋了為什麼會有上面例子中那個結果了。

輸出邏輯

文字輸出

我們可以使用 lineinfocommentquestionerror 方法,輸出文字到控制檯。顯示效果如下:

9Mq1gB3HG4.png!large

透過 newLine($count) 方法可以建立 $count行的空行,$count 預設為 1 。

表格輸出

透過 table 方法可以建立一個表格,程式碼如下:

$this->table(
    // 表頭
    ['姓名', '性別', '年齡'],
    // 表格內容
    [
        ['張三', '男', 20],
        ['李四', '男', 30],
        ['王五', '男', 40],
    ],
    // 表格樣式,支援的樣式:'default', 'borderless', 'compact', 'symfony-style-guide', 'box', 'box-double'
    // $tableStyle = 'default',
    // 列樣式
    // $columnStyles = [],
);

各種不同樣式的表格顯示效果如下:

sAuYluklo9.png!large

進度條

對於長時間執行的任務,我們可以透過加一個進度條來實時展現處理進度,這樣看上去會更直觀。處理流程如下:

$this->line('開始處理任務...');
$timers = [1, 2, 3, 4, 5]; // 定義遍歷陣列

$bar = $this->output->createProgressBar(count($timers)); // 建立進度條,並初始化步數
$bar->start(); // 啟動進度條

// 遍歷陣列
foreach ($timers as $timer) {
    sleep($timer);   // 程式處理邏輯
    $bar->advance(); // 推進進度條
}

$bar->finish(); // 結束進度條
$this->newLine();
$this->info('完成任務!');

其顯示效果如下:

rDflsJWei9.gif!large

當我們需要處理比較耗時且需要按步驟執行的操作時,加上這麼一個進度條,是不是就很方便了呢。

至此,我們已經瞭解了命令類的基本結構、輸入及輸出的邏輯,剩下的就是處理我們實際業務中的邏輯了。

這裡我們再來介紹一下實際應用中比較方便的一種命令實現方式。

實際應用

我們可以發現,上面例子中的應用場景都是一個命令對應一個類檔案,這樣設計命令類的處理邏輯比較單一,但當我們的命令越來越多的時候,會在命令目錄下生成一堆的「命令檔案」,不方便維護。

這裡我們可以把命令類稍微改造一下,使命令類作為一個統一的入口,我們要執行的指令碼作為引數傳遞,這樣就可以透過傳遞不同的引數執行不同的指令碼了。示例程式碼如下:

class MissionScript extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'script:mission
                            {name : 任務名稱}
                            {--p|parameter=? : 任務引數}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '任務指令碼';

    /**
     * 任務配置
     *
     * @var array
     */
    protected $missions = [
        'sendMail' => [
            'desc' => '傳送郵件任務',
            'callback' => 'sendMail',
        ],
    ];

   /**
    * 任務服務
    * 
    * @var MissionService 
    */
    protected $missionService;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(MissionService $missionService)
    {
        parent::__construct();
        $this->missionService = $missionService;
    }

    /**
     * Execute the console command.
     *
     * @return int
     * @throws \Exception
     */
    public function handle()
    {
        $missionName = $this->argument('name');
        $parameter = $this->option('parameter');

        if(!isset($this->missions[$missionName])){
            throw new \Exception("[{$missionName}]任務沒有配置,請先進行配置");
        }

        $callback = Arr::get($this->missions, "{$missionName}.callback");
        if(!$callback){
            throw new \Exception("[{$missionName}]任務的回撥方法沒有配置");
        }

        if (method_exists($this, $callback)) {
            $handler = $this;
        } else if (is_callable([$this->missionService, $callback])) {
            $handler = $this->missionService;
        } else {
            throw new \Exception("[{$missionName}]任務的回撥方法[{$callback}]不可呼叫");
        }

        $data = json_decode($parameter, true);
        $data = $data ? $data : [];

        return call_user_func_array([$handler, $callback], [$data]);
    }

    /**
     * 傳送郵件
     *
     * @param array $data 引數
     */
    public function sendMail(array $data = [])
    {
        $users = $data['users'] ?? [];
        $sendAt = $data['sendAt'] ?? '';

        $this->info('`sendMail` 任務指令碼將於 [' . $sendAt . '] 向 [' . implode(',', $users) . '] 傳送郵件');
    }
}

關於這個任務類,有以下幾點需要說明一下:

  • 命令接收一個必選的 {命令名稱} 引數 和一個可選的 {命令引數}{命令引數} 格式為 json,擴充套件性更強。
  • 增加一個 missions 屬性相當於維護一套任務配置,只有正確配置的指令碼才可以被呼叫,配置還有一個作用就是可以幫助接手專案的人快速瞭解指令碼中有哪些可用的任務。
  • 增加一個 MissionService 服務是為了把部分不需要互動場景的任務放在 MissionService 中維護(比如需要在後臺執行的定時任務),而且 MissionService 中的任務還可以作為服務被其他呼叫方呼叫,提高了複用性,而那些需要互動場景的任務則直接放在命令類中維護。
  • 呼叫任務的時候,會優先在 MissionService 中查詢,如果存在的話則直接呼叫,沒有的話則在當前任務類中進行查詢。

這樣我們就可以透過以下方式呼叫命令了:

php artisan script:mission sendMail -p '{"users":["Tom", "Sam"],"sendAt":"2023-05-20 15:00"}'

命令的執行結果如下:

`sendMail` 任務指令碼將於 [2023-05-20 15:00][Tom,Sam] 傳送郵件

當這個「任務指令碼」變的越來越龐大的時候,我們還可以考慮進行更進一步的歸類,從而達到既方便維護,又方便開發的目的。

結語

到這裡,關於 Artisan 入門的知識我們就介紹完了。關於 Artisan 其他方面的一些使用小技巧後續我們會透過具體的話題進行討論,感謝大家持續關注~

本作品採用《CC 協議》,轉載必須註明作者和本文連結
你應該瞭解真相,真相會讓你自由。

相關文章