【Laravel11】Artisan ConsoleのpromptForMissingArgumentsUsingをもう少し詳しく

Laravel

promptForMissingArgumentsUsingとは

かなりニッチなネタかもしれませんが、Laravel11のArtisanコマンドでは、引数が省略された際に対話的に引数を受け取る仕組みが用意されています。

Artisan Console - Laravel 11.x - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We???ve already laid the foundation ??? free...

この公式ドキュメントではさらっとしか記載されていないので気づきにくいのですが、

そこでLaravel公式パッケージのPromptsを使うことができます。

Prompts - Laravel 11.x - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We???ve already laid the foundation ??? free...

Promptは、CLI上でもWEBのようにテキストボックス、ラジオボタン、チェックボックス等を使って入力欄を表示し、入力を受け付けることができるようにするパッケージです。

Artisan Consoleの実装でも、複数の選択肢から単一選択・複数選択ができますが、Promptsの方がより直感的で入力形式の選択肢が広がり、より効果的な対話式入力を実装することができるようになります。

この記事のゴール

Artisanコマンドの「promptForMissingArgumentsUsing()」メソッドにおいて、Promptsを使用していきます。

前提条件

  • Ubuntu上で作業しています
  • PHP8.2以降インストール済
  • Composerインストール済

これからやること

  • Laravel11プロジェクト新規作成
  • Artisanコマンド作成
  • promptForMissingArgumentsUsing()実装
  • 複数選択肢の対話入力実装
  • promptForMissingArgumentsUsing()の戻り値がどのように使われているのか確認
  • マイグレーション(シーダー実行)
  • Promptsによる対話入力実装

Laravel11プロジェクト新規作成

Laravel11プロジェクト「artisan-prompt」を作成します。

composer create-project laravel/laravel:^11 artisan-prompt

Artisanコマンド作成

コマンドライン引数を表示するだけの簡単なコマンドを作成します。

まずはプロジェクトフォルダに入ります。

cd artisan-prompt

Artisanコマンド「app:hoge」を作成します。

php artisan make:command Hoge

新しいフォルダ「app/Console/Commands/」が作成され、その中に「Hoge.php」が作成されます。

ファイルを開いて編集・保存します。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class Hoge extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:hoge {arg}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Sample command for using prompt.';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        var_dump($this->argument('arg'));
    }
}

引数「fuga」を付けて実行してみます。

php artisan app:hoge fuga

引数「fuga」が表示されました。

次は引数なしで実行してみます。

php artisan app:hoge

このようにコマンド実行はエラーにより中断されます。

promptForMissingArgumentsUsing()実装

引数なしで実行されてもエラー終了させずに、

引数を対話形式で入力受付できるようにしていきます。

公式ドキュメントの説明に沿って実装します。

Artisan Console - Laravel 11.x - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We???ve already laid the foundation ??? free...

「app/Console/Commands/Hoge.php」を編集・保存します。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;

class Hoge extends Command implements PromptsForMissingInput
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:hoge {arg}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Sample command for using prompt.';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        var_dump($this->argument('arg'));
    }

    /**
     * Prompt for missing input arguments using the returned questions.
     *
     * @return array<string, string>
     */
    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg' => '引数を文字列で入力してください。',
        ];
    }
}

変更したのは3箇所です。

1.use宣言追記

use Illuminate\Contracts\Console\PromptsForMissingInput;

2.インターフェイス追記

class Hoge extends Command implements PromptsForMissingInput

3.promptForMissingArgumentsUsingメソッド追記

    /**
     * Prompt for missing input arguments using the returned questions.
     *
     * @return array<string, string>
     */
    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg' => '引数を文字列で入力してください。',
        ];
    }

では引数なしで実行してみましょう。

php artisan app:hoge

「引数を文字列で入力してください。」と表示され、

テキストボックスの入力欄が表示され入力待ちになっています。

適当に「ふが」とでも入力して[Enter]で確定してみましょう。

この入力欄にはプレイスホルダーを表示させることができます。

promptForMissingArgumentsUsingメソッドの戻り値を

文字列型から配列に変更し、第二要素にプレイスホルダーを指定することができます。

    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg' => [ '引数を文字列で入力してください。', '例) fuga', ],
        ];
    }

このように修正・保存してから、引数なしで実行してみましょう。

このようにプレイスホルダー「例)fuga」がグレーで表示され、入力待ちの状態になりました。

ちなみに、入力待ちの状態で[Ctrl]+[C]を押すと強制終了できます。

複数選択肢の対話入力実装

Artisan Consoleでも、一応、数選択肢からの単一選択・複数選択が可能になっています。

promptForMissingArgumentsUsingメソッドを編集・保存してみましょう。

    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg' => fn () => $this->choice(
                '引数を選択してください。',
                [ 'hoge', 'fuga', 'piyo', ],
                $defaultIndex = 0,
            ),
        ];
    }

実行してみましょう。

php artisan app:hoge

複数の選択肢が表示されました。

デフォルトで0番目の「hoge」が選択されています。

※「$defaultIndex = null」にするとデフォルト選択なし

2番目の「piyo」を選択するには数字を入力します。

2を入力して[Enter]で確定させてみましょう。

2番目の「piyo」が引数に設定され、実行結果が表示されました。

次は複数選択できるようにしてみましょう。

    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg' => fn () => $this->choice(
                '引数を選択してください。',
                [ 'hoge', 'fuga', 'piyo', ],
                $defaultIndex = 0,
                $maxAttempts = null,
                $allowMultipleSelections = true,
            ),
        ];
    }

実行してみましょう。

対話モードの表示は先程と同じですが、

複数選択する場合は、配列インデックスをカンマ区切りで入力します。

引数が配列でセットされていることが判りますね。

ただ、カンマ区切り入力は少し面倒な気もします。

promptForMissingArgumentsUsing()の戻り値がどのように使われているのか確認

公式ドキュメントに説明は無いですが、

「テキストボックスが可能なら、ラジオボタンやチェックボックスもいけるでしょ?」と思いますよね。

promptForMissingArgumentsUsing()の戻り値がどのように使われているのか、フレームワーク側の実装を確認してみます。

grep promptForMissingArgumentsUsing -r ./vendor/laravel/framework/Illuminate/
vendor/laravel/framework/src/Illuminate/Console/Concerns/PromptsForMissingInput.php

で実装しているようなので、コードを確認してみます。

※42行目~59行目までを抜粋します。

            ->each(function ($argument) use ($input) {
                $label = $this->promptForMissingArgumentsUsing()[$argument->getName()] ??
                    'What is '.lcfirst($argument->getDescription() ?: ('the '.$argument->getName())).'?';

                if ($label instanceof Closure) {
                    return $input->setArgument($argument->getName(), $label());
                }

                if (is_array($label)) {
                    [$label, $placeholder] = $label;
                }

                $input->setArgument($argument->getName(), text(
                    label: $label,
                    placeholder: $placeholder ?? '',
                    validate: fn ($value) => empty($value) ? "The {$argument->getName()} is required." : null,
                ));
            })

やっていることは4段階です。

1.$labelにpromptForMissingArgumentsUsing()の戻り値格納

2.$labelがクロージャ―の場合はクロージャ―実行結果で引数セット&リターン

3.$labelが配列の場合は第一要素を$labelに、第二要素を$placeholderに格納

4.$labelと$placeholderを使って引数を対話入力実行

ここで注目すべきは2のクロージャ―が使える点です。

公式ドキュメントの説明では、ユーザー名からユーザーモデルを使ってユーザー検索するアロー関数の例が記載されています。

このクロージャ―を使うことで何でもできそうな感じですね。

マイグレーション(シーダー実行)

Prompts実装の前に、ユーザーデータを作成するためにシーダーの設定とマイグレーションを実行します。

まずは、「database/seeders/DatabaseSeeder.php」を編集・保存します。

「run()」メソッドの箇所を次のように修正します。

    public function run(): void
    {
        User::factory(10)->create();

        /*
        User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
        */
    }

シードオプション付きでマイグレーションを実行します。

php artisan migrate --seed

ユーザーが10件作成されました。

Promptによる対話入力実装

実装可能なPromptsの対話入力は次の通りです。

  • Text
  • Password
  • Confirm
  • Select
  • Multi-select
  • Suggest
  • Search
  • Multi-search

このうち、「Text」はpromptForMissingArgumentsUsing()の動作確認したものと同じです。

また、SearchはArtisan Consoleの公式ドキュメントのコード例で記載されています。

「pause」や「inform」、「table」等も実装できますが、

対話入力としては機能しないので、今回は省きます。

「app/Console/Commands/Hoge.php」を編集・保存します。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use App\Models\User;
use function Laravel\Prompts\text;
use function Laravel\Prompts\password;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\suggest;
use function Laravel\Prompts\search;
use function Laravel\Prompts\multisearch;

class Hoge extends Command implements PromptsForMissingInput
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:hoge
                            {arg-text}
                            {arg-password}
                            {arg-confirm}
                            {arg-select}
                            {arg-multiselect}
                            {arg-suggest}
                            {arg-search}
                            {arg-multisearch}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Sample command for using prompt.';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        var_dump($this->arguments());
    }

    /**
     * Prompt for missing input arguments using the returned questions.
     *
     * @return array<string, string>
     */
    protected function promptForMissingArgumentsUsing(): array
    {
        return [
            'arg-text' => fn () => text(
                label: 'What is your name?',
                placeholder: 'E.g. Taylor Otwell',
                hint: 'This will be displayed on your profile.',
                required: 'Your name is required.',
                validate: fn (string $value) => match (true) {
                    strlen($value) < 3 => 'The name must be at least 3 characters.',
                    strlen($value) > 40 => 'The name must not exceed 40 characters.',
                    default => null
                }
            ),
            'arg-password' => fn () => password(
                label: 'What is your password?',
                placeholder: 'password',
                hint: 'Minimum 8 characters.',
                required: 'The password is required.',
                validate: fn (string $value) => match (true) {
                    strlen($value) < 8 => 'The password must be at least 8 characters.',
                    default => null
                }
            ),
            'arg-confirm' => fn () => confirm(
                label: 'Do you accept the terms?',
                default: false,
                yes: 'I accept',
                no: 'I decline',
                hint: 'The terms must be accepted to continue.',
                required: 'You must accept the terms to continue.'
            ),
            'arg-select' => fn () => select(
                label: 'What role should the user have?',
                options: [
                    'member' => 'Member',
                    'contributor' => 'Contributor',
                    'owner' => 'Owner'
                ],
                default: 'owner',
                hint: 'The role may be changed at any time.'
            ),
            'arg-multiselect' => fn () => multiselect(
                label: 'What permissions should be assigned?',
                options: [
                    'read' => 'Read',
                    'create' => 'Create',
                    'update' => 'Update',
                    'delete' => 'Delete'
                ],
                default: ['read', 'create'],
                hint: 'Permissions may be updated at any time.',
                required: 'You must select at least one category',
            ),
            'arg-suggest' => fn () => suggest(
                label: 'What is your name?',
                options: ['Taylor', 'Dayle'],
                placeholder: 'E.g. Taylor',
                hint: 'This will be displayed on your profile.',
                required: 'Your name is required.',
                validate: fn (string $value) => match (true) {
                    strlen($value) < 3 => 'The name must be at least 3 characters.',
                    strlen($value) > 40 => 'The name must not exceed 40 characters.',
                    default => null
                }
            ),
            'arg-search' => fn () => search(
                label: 'Search for a user:',
                placeholder: 'E.g. Taylor Otwell',
                options: fn ($value) => strlen($value) > 0
                    ? User::where('name', 'like', "%{$value}%")->pluck('name', 'id')->all()
                    : []
            ),
            'arg-multisearch' => fn () => multisearch(
                label: 'Search for the users that should receive the mail',
                placeholder: 'E.g. Taylor Otwell',
                options: fn (string $value) => strlen($value) > 0
                    ? User::where('name', 'like', "%{$value}%")->pluck('name', 'id')->all()
                    : [],
                hint: 'The user will receive an email immediately.',
                scroll: 10,
                required: 'You must select at least one user.'
            ),
        ];
    }
}

少し長くなりましたが、上記のリストにあるもの全てを一気に実装しました。

実行してみましょう。

php artisan app:hoge

見覚えのあるテキストボックスですね。

今回はバリデーションを設定したので確認してみます。

3文字以上、40文字以内でないと警告が出ます。

1文字だけ入力して[Enter]を押すと次のようになります。

40文字を超えると次のようになります。

文字制限の範囲内に修正して[Enter]で確定します。

次はパスワードです。これも8文字以上のバリデーションがあるので、まずは7文字で確定してみます。

警告が表示されました。8文字以上で確定してみます。

次はconfirmです。デフォルトの「I decline」で確定してみます。

「I accept」を選択しないと警告が出ます。

「I accept」を選択して確定します。

次はselectが表示されました。

適当に選択して確定します。

次はMulti-selectが表示されました。

適当に選択して確定します。

次はsuggestが表示されました。

入力欄の右端に小さく下向きの矢印が表示されています。

この状態で下向き矢印[↓]キーを押すと、サジェストの選択肢が表示されます。

適当に選択して確定します。(自由入力可)

次はsearchが表示されました。

ユーザー名の入力を基に、Userモデルであいまい検索できます。

「p」とでも入力してみます。

リアルタイム検索結果が表示されました。

適当に選択して確定します。

次はMulti-searchが表示されました。

「s」とでも入力してみます。

リアルタイム検索した結果で複数選択のリストが表示されました。適当に複数選択して確定してみます。

上記で入力した内容が引数として設定されて、

var_dump()されました。

var_dump()の内容を確認してみます。

/home/macocci7/work/artisan-prompt/app/Console/Commands/Hoge.php:46:
array(9) {
  'command' =>
  string(8) "app:hoge"
  'arg-text' =>
  string(4) "Hoge"
  'arg-password' =>
  string(8) "password"
  'arg-confirm' =>
  bool(true)
  'arg-select' =>
  string(11) "contributor"
  'arg-multiselect' =>
  array(4) {
    [0] =>
    string(4) "read"
    [1] =>
    string(6) "create"
    [2] =>
    string(6) "delete"
    [3] =>
    string(6) "update"
  }
  'arg-suggest' =>
  string(6) "Taylor"
  'arg-search' =>
  int(5)
  'arg-multisearch' =>
  array(3) {
    [0] =>
    int(4)
    [1] =>
    int(6)
    [2] =>
    int(9)
  }
}

全て出力されていますね。

今回は以上です。

  • 0
  • 0
  • 0
  • 0

コメント

タイトルとURLをコピーしました