symfony/consoleを使ってPHPでCLIを書いてみる
はじめに
前回は素のPHPでCLIツールを書いてみたのですが、如何せんしょぼい…なので、今回はsymfony/console
というライブラリを使って、リッチなCLIツールを書いてみたいと思います。
今回の成果物はここにあります。
symfony/consoleとは
symfony/console
はSymfonyというWeb Application Frameworkの一部で、Symfonyのコンソールコマンドで使用されています。Symfonyの一部と言っても、
symfony/console
は独立して使えるようです。実際、Laravelのlaravel/installer
やartisan
はこのsymfony/console
を使っています。
今回はsymfony/console
を使ってCLIツールを作ってみたいと思います。
CLIツールの作り方
1. プロジェクトを作る
まず、composer init
を実行してプロジェクトを作ります。リッチなCLIツールを作りますので、名前はrichにでもしておきましょう。
途中で依存パッケージを聞かれるので、symfony/console
を入れてやりましょう。
$ mkdir rich; cd rich $ composer init ... Would you like to define your dependencies (require) interactively [yes]? Search for a package: symfony/console ... Would you like to install dependencies now [yes]? Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 5 installs, 0 updates, 0 removals - Installing psr/container (1.0.0): Loading from cache - Installing symfony/service-contracts (v2.0.1): Loading from cache - Installing symfony/polyfill-php73 (v1.16.0): Downloading (100%) - Installing symfony/polyfill-mbstring (v1.16.0): Downloading (100%) - Installing symfony/console (v5.0.8): Loading from cache symfony/service-contracts suggests installing symfony/service-implementation symfony/console suggests installing symfony/event-dispatcher symfony/console suggests installing symfony/lock symfony/console suggests installing symfony/process symfony/console suggests installing psr/log (For using the console logger) ...
ディレクトリはこうなります。今後はここをワーキングディレクトリとして作業していきます。
$ ls composer.json composer.lock vendor
2. エントリポイントを作る
まず、bin/rich
ファイルを作ります。ファイルの内容は以下の通りです。
#!/usr/bin/env php <?php require __DIR__.'/../vendor/autoload.php'; use Symfony\Component\Console\Application; $app = new Application('Rich command', '0.0.1'); $app->run();
Application
クラスにコマンド名とバージョンを渡せます。
次に、autoload.php
を作ります。これで、vendor/autoload.php
が作られます。
$ composer dump-autoload
最後に、php bin/rich
を実行してみましょう。
$ php bin/rich Rich command 0.0.1 Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable 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: help Displays help for a command list Lists commands
なかなかいい感じですね。
3. コマンドを作る
とりあえず、前回同様catコマンドを作ってみます。まず、catコマンド用にsrc/CatCommand.php
を作ります。
<?php namespace Rich\Console; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class CatCommand extends Command { protected function configure() { $this ->setName('cat') ->setDescription('Concatenate and print files'); } protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('This is cat command!'); return 0; } }
composer.json
を編集し、autoload
を追加します。これで、use Rich\Console;
と書くとと、src/
以下のRich\Console
名前空間に入っているものが使えるようになります。
{ "name": "d2verb/rich", "description": "A rich cli tool", "require": { "symfony/console": "^5.0" }, "autoload": { "psr-4": { "Rich\\Console\\": "src/" } } }
ここまでできたら、composer dump-autoload
を実行してautoload.php
を作成しましょう。
次に、作ったコマンドを登録します。bin/rich
に以下の行を$app->run()
の前に追加します。
$app->add(new CatCommand());
php bin/rich
を実行してみます。
$ php bin/rich list master ✱ ◼ Rich command 0.0.1 Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable 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: cat Concatenate and print files <-- catコマンドが追加されています。 help Displays help for a command list Lists commands $ php bin/rich cat This is cat command!
最後にCatCommand
をちゃんと実装します。
<?php namespace Rich\Console; use SplFileObject; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class CatCommand extends Command { protected function configure() { $this ->setName('cat') ->setDescription('Concatenate and print files') ->addArgument('filenames', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Filenames you want to concatenate.'); } protected function execute(InputInterface $input, OutputInterface $output) { $filenames = $input->getArgument('filenames'); foreach ($filenames as $filename) { $this->printFileContent($output, $filename); } return 0; } protected function printFileContent(OutputInterface $output, string $filename) { $file = new SplFileObject($filename); foreach ($file as $line) { $output->write($line); } } }
実行してみましょう。以下の例では、./bin/rich
と./src/CatCommand.php
をcatコマンドに渡しています。
$ php bin/rich cat ./bin/rich ./src/CatCommand.php #!/usr/bin/env php <?php require __DIR__.'/../vendor/autoload.php'; use Symfony\Component\Console\Application; use Rich\Console\CatCommand; ...
ファイル名を渡さなかったら怒られます。
$ php bin/rich cat master ✱ ◼ Not enough arguments (missing: "filenames"). cat <filenames>...
存在しないファイル名を渡すと例外が発生します。
$ php bin/rich cat NULL ✘ 1 master ✱ ◼ PHP Warning: fopen(NULL): failed to open stream: No such file or directory in /Users/a_fukumoto/Workspace/misc/study/php/cli/rich/src/CatCommand.php on line 32 Warning: fopen(NULL): failed to open stream: No such file or directory in /Users/a_fukumoto/Workspace/misc/study/php/cli/rich/src/CatCommand.php on line 32 In CatCommand.php line 34: Can not open the file: 'NULL' cat <filenames>...
想定通りの動きをしていますね。
おわりに
symfony/console
を使えば、よりリッチなCLIツールを作れることがわかりました。CLIツール作りが捗ります。symfony/console
の使い方は公式のドキュメントにまとまっていますので、困ったら参照しましょう。
おまけ: Laravelで新しいコマンドを作る
Laravelにはmake:command
というコマンドが存在しています。このコマンドを使うことによってユーザー定義のコマンドを作ることができます。
ここでは、例として新しいviewファイルを作るコマンドを作ってみましょう。
まず、php artisan make:command MakeViewCommand
を実行します。
すると、app/Console/Commands/MakeViewCommand.php
が生成されますので、中身を以下のように変更しましょう。
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use Symfony\Component\Console\Exception\RuntimeException; class MakeViewCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'make:view {file}'; /** * The console command description. * * @var string */ protected $description = 'Create a new view file'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { $viewDir = __DIR__ . '/../../../resources/views/'; // documents.indexのような文字列を引数に取る $file = $this->argument('file'); // documents/index.blade.phpのような形に変換する $viewFilePath = $viewDir . str_replace('.', '/', $file) . '.blade.php'; if (file_exists($viewFilePath)) { throw new RuntimeException("File already exists"); } $viewFileDir = dirname($viewFilePath); if (!file_exists($viewFileDir)) { mkdir(dirname($viewFilePath), 0777, true); } touch($viewFilePath); } }
MakeViewCommand.php
を上記のように変更して以下を実行するとresources/views
以下にdocuments/index.blade.php
が生成されます。
$ php artisan make:view documents.index