🎻

symfony/consoleを使えばCLIツールが超簡単に作れる!

2021/12/21に公開

Symfony Advent Calendar 2021 の21日目の記事です!🎄🌙

ちなみに、僕はよく TwitterにもSymfonyネタを呟いている ので、よろしければぜひ フォローしてやってください🕊🤲

昨日は @77web さんの zenstruck/foundryを使ってみる でした✨

symfony/console とは

symfony/console は、テスタブルかつ見た目にも美しいCLIを簡単に実装できるような諸機能を提供してくれるSymfonyコンポーネントで、PHPerにはお馴染みの Composer などもsymfony/console を使って実装されています。

Symfonyユーザーでなくとも、業務や日常生活のちょっとした作業を自動化するためのCLIツールをササっと作ったりするのにとても便利なので、簡単に紹介してみたいと思います。

Hello, World!

とりあえず Hello, World! してみましょう。

まずは以下のような内容で composer.json を作成して、

{
    "require": {
        "symfony/console": "^6.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

composer install します。

$ composer install

次にコマンドの中身を実装します。以下のような内容で ./src/Command/WorldCommand.php を作成してみましょう。

<?php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class WorldCommand extends Command
{
    protected static $defaultName = 'world';

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        echo 'Hello, World!';

        return Command::SUCCESS;
    }
}

これで、world という名前の、Hello, World!echo するだけの コマンドが実装できました。

最後にコマンドを包括する実行ファイルを作成します。以下のような内容で ./hello というファイルを作成しましょう。

#!/usr/bin/env php
<?php
require __DIR__ . '/vendor/autoload.php';

use App\Command\WorldCommand;
use Symfony\Component\Console\Application;

$application = new Application();
$application->add(new WorldCommand());
$application->run();

#!/usr/bin/env php というShebangで実行させるので、当然ながらPATHの通った場所に php コマンドがインストールされている環境でないと動きません。

./hello は実行ファイルなので以下のような感じでパーミッションを変更しておきましょう。

$ chmod a+rx ./hello

これで完了です。動かしてみましょう。

$ ./hello

コマンドを指定せずに実行ファイルを実行すると、以下のように使用の手引きと利用可能なコマンドの一覧が出力されます。composer コマンドと同じですね。

自分では何も実装していないのにこの辺のインターフェースが出来上がっているのが超ありがたいですね!

Console Tool

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display help for the given command. When no command is given display help for the list command
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  completion  Dump the shell completion script
  help        Display help for a command
  list        List commands
  world

次は world コマンドを指定して実行してみましょう。

$ ./hello world
Hello, World!

こちらは、先ほど実装したとおり Hello, World! という文字列が出力されました。

1コマンドしかないツールの場合はデフォルトコマンドを設定すればコマンドの指定を省略できる

今回のように1コマンドしかないCLIツールの場合は、以下のようにデフォルトコマンドを指定してあげれば、コマンド名を省略して実行することができるようになります。

  #!/usr/bin/env php
  <?php
  require __DIR__ . '/vendor/autoload.php';
  
  use App\Command\WorldCommand;
  use Symfony\Component\Console\Application;
  
  $application = new Application();
  $application->add(new WorldCommand());
+ $application->setDefaultCommand('world');
  $application->run();
$ ./hello
Hello, World!

入力の受け取りや出力の装飾がめっちゃ簡単にできる

symfony/console の本領が発揮されるのはある程度複雑な入出力のインターフェースが必要になる場合です。

入力の受け取り

例えば先ほどの ./hello world コマンドに以下のような引数を追加実装してみましょう。

  • --names というオプションで 'World' の代わりに呼びかける名前を0個〜複数個指定できる
  • --count というオプションで出力する回数を指定できる

この場合、./src/Command/WorldCommand.php を以下のように実装することで対応できます。

protected function configure(): void
{
    $this
        ->setDescription('Hello, World! する')
        ->addOption('names', 'N', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, '呼びかける名前(複数可)', ['World'])
        ->addOption('count', 'c', InputOption::VALUE_REQUIRED, '呼びかける回数', 1)
    ;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $names = $input->getOption('names');
    $count = (int) $input->getOption('count');

    for ($i = 0; $i < $count; $i++) {
        echo 'Hello, ' . implode(', ', $names) . '!' . PHP_EOL;
    }

    return Command::SUCCESS;
}

configure() メソッドでコマンドの仕様を以下のように定義しています。

  • (もののついでに)setDescription() でコマンド自体の説明文を設定
  • addOption()names および count オプションのインターフェースを設定
    • names オプションには
      • N というショートカットを設定( n はデフォルトで --no-interaction のショートカットとして使われており使用できないため大文字にしている)
      • モードとして InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY つまり「オプションは値付きでなければならず、配列形式(つまり複数指定可)」という内容を設定
      • オプションが指定されなかった場合のデフォルト値は ['World'] とする
    • count オプションには
      • c というショートカットを設定
      • モードとして InputOption::VALUE_REQUIRED つまり「オプションは値付きでなければならない」という内容を設定
      • オプションが指定されなかった場合のデフォルト値は 1 とする

これを受けて、コマンドの処理本体である execute() メソッドでは

$names = $input->getOption('names');
$count = (int) $input->getOption('count');

という感じで引数として入力された値を取り出しています。

実際に動かしてみましょう。

このように、適切に引数が渡せて、しかも「値付きでなければならない」としたオプションが値なしで実行された場合には適切にエラーを吐いてくれます。便利!

出力の装飾

入力の受け取りだけでなく、出力の装飾も簡単です。

例えば、(この例では実用上まったく適切ではないですが)出力をテーブル形式にしてみましょう。

symfony/console に組み込まれている Table というヘルパークラス を使えば超簡単に実装できます。

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $names = $input->getOption('names');
    $count = (int) $input->getOption('count');

    $table = new Table($output);
    for ($i = 0; $i < $count; $i++) {
        $table->addRow(array_merge(['Hello'], $names));
    }
    $table->render();

    return Command::SUCCESS;
}

便利!

Table 以外にも、対話式のコマンドを簡単に実装できる Question や、プログレスバーを簡単に実装できる ProgressBar など、便利なヘルパークラスが色々と用意されています。

詳細は 公式ドキュメント から辿ってみてください。

こういった便利なヘルパーのおかげでUIの実装については自分でほぼ何も書く必要がなく、コマンドの処理の本質にフォーカスできてとても嬉しいですね!

おわりに

というわけで、symfony/console を簡単に紹介してみました!Symfonyユーザー以外のPHPerの方もぜひ活用してみていただければと思います😉

Symfony Advent Calendar 2021、明日は @chanshige さんです!お楽しみに!

GitHubで編集を提案

Discussion