symfony/consoleを使えばCLIツールが超簡単に作れる!
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 さんです!お楽しみに!
Discussion