【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 8章 コンソールアプリケーション 前半資料
以下の記事は2019年9月12日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 コンソールアプリケーションの輪読会資料の一部となります。
今回は 4章前半 8-1 『Commandの基礎』, 8-2『Commandの実装』部分の記事をアップします。
また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応
- 作者: 竹澤有貴,栗生和明,新原雅司,大村創太郎,丸山弘詩
- 出版社/メーカー: ソシム
- 発売日: 2018/09/26
- メディア: 単行本
- この商品を含むブログを見る
また書籍のコードが記載されているGitHubレポジトリは以下となります。とても助かります!
GitHub - laravel-socym/chapter08
8-1 Commandの基礎
- 8-1-1 クロージャによるCommandの基礎
- 8-1-2 クラスによるCommandの作成
- 8-1-3 Commandへの入力
- 8-1-4 Commandからの入力
- 8-1-5 Commandの実行
8-2 Commandの実装
8-1 Commandの基礎
LaravelにはCommandというコンポーネントがあり、これを使えばコンソールアプリケーションに必要な機能を簡単に実装できる
Commandの作り方には2種類ある
- クロージャ
- 簡単な処理を書くのに向いている
- クラス
- 複雑な処理にも対応可能
8-1-1 クロージャによるCommandの基礎
こんな風にオリジナルのコマンドを作る事ができる ここでは単純な文字列を表示するCommandをクロージャで作る
実行してみる
php artisan hello:closure Hello Closure command!
routes/console.php
にクロージャで書いて定義する
<?php Artisan::command('hello:closure', function () { $this->comment('Hallo Closure Command!'); // Command本体 文字列出力 })->describe('サンプルコマンド(クロージャ実装)'); //コマンドの説明
routes/console.php
にコマンドを書くとlistに自動登録される、以下のコマンドで登録が確認できる
実行してみる
vagrant@homestead:~/study_command$ php artisan list Laravel Framework 6.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 --env[=ENV] The environment the command should run under -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: clear-compiled Remove the compiled class file down Put the application into maintenance mode env Display the current framework environment ### 省略 ### flare flare:test Send a test notification to Flare hello hello:closure サンプルコマンド(クロージャ実装) key key:generate Set the application key ### 省略 ###
8-1-2 クラスによるCommandの作成
以下の様なコマンドでClassのひな形を作れる
php artisan make:command HelloCommand Console command created successfully.
ちなみに、どうも同時にどこかに自動で登録をしているようで ファイル名を間違えて自分で手直しすると、 list に出てこなかったり、実行できなかったり登録する?みたいなコマンドが出たりしたが、正体不明であった)
作成されたファイル app/Console/Commands/HelloCommand.php
の中身
<?php namespace App\Console\Commands; use Illuminate\Console\Command; class HelloCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'command:name'; /** * The console command description. * * @var string */ protected $description = 'Command description'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { // } }
上記を以下の様に書き換えると、クロージャ同様のコマンドが作れる
<?php namespace App\Console\Commands; use Illuminate\Console\Command; class HelloCommand extends Command { /** * The name and signature of the console command. * * @var string */ // コマンドを指定する protected $signature = 'hello:class'; /** * The console command description. * * @var string */ // コマンドの説明を指定 protected $description = 'サンプルコマンド(クラスで実装)'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ // 実行されるコマンドの定義 public function handle() { $this->comment('Hello class command!'); } }
実行コマンドと結果
実行してみる
php artisan hello:class Hello class command!
リストにも登録されているのが確認できる
vagrant@homestead:~/study_command$ php artisan list ### 省略 ### flare flare:test Send a test notification to Flare hello hello:class サンプルコマンド(クラスで実装) hello:closure サンプルコマンド(クロージャ実装) key key:generate Set the application key ### 省略 ###
8-1-3 Commandへの入力
vagrant@homestead:~/study_command$ php artisan hello:option --param Hello Option param => 有 vagrant@homestead:~/study_command$ php artisan hello:option Hello Option param => 無
コマンドには引数を入れて処理をさせることが出来る
実際にこんなコマンドを叩いた経験があるかもしれないが、
php artisan make:migration [マイグレーションファイル名] --table=[テーブル名]
後ろのファイル名(必須)と テーブル名(オプション) が引数となっている
コマンド引数 (必須)
$signature に引数を { }
で指定できる
<?php protected $signature = 'hello:class {name}';
handleメソッド内で argument() を使って呼べる
<?php public function handle() { $param = $this->argument('name'); $this->comment('Hello class command! Param => ' . ($param)); }
コマンド実行結果
やってみる際にコードのコメントを変更
vagrant@homestead:~/study_command$ php artisan hello:class taro Hello class command! Param => taro
その他引数の指定方法
コマンド引数 | 内容 |
---|---|
{name} | 引数を文字列として取得 省略するとエラー |
{name?} | 引数を文字列として取得 省略可能 |
{name=default} | 引数を文字列として取得 デフォルト値指定 |
{name*} | 引数を配列として取得 省略するとエラー |
{name : description} | : コロン以降に説明を記述可能 : 前後にスペース必須 |
オプション引数 (任意)
あっても無くても良いオプションの引数を指定できる
以下は引数を論理値として使える例で、引数の文字列は指定したものを決め打ちしないと駄目
<?php protected $signature = 'hello:option {--param}';
option()メソッドで利用できる
<?php public function handle() { $param = $this->option('param'); $this->comment('Hello Option param => ' . ($param ? '有' : '無')); }
やってみる際にコードのコメントを変更
引数ありで実行 指定すると true となる
vagrant@homestead:~/study_command$ php artisan hello:option --param Hello Option param => 有
引数無しで実行 指定なしは false となる
vagrant@homestead:~/study_command$ php artisan hello:option Hello Option param => 無
オプション引数の指定方法には以下のようなものがある
コマンド引数 | 内容 |
---|---|
{--switch} | 引数を論地値として取得 省略するとエラー |
{--switch=} | 引数を文字列として取得 省略可能 |
{--switch=default} | 引数を文字列として取得 デフォルト値指定 |
{--switch=*} | 引数を配列として取得 省略するとエラー |
{--switch : description} | :コロン以降に説明を記述可能 : 前後にスペース必須 |
8-1-4 Commandからの入力
出力の際の文字色や表示レベルをカスタマイズできる
<?php public function handle() { // 文字色,背景色の設定 $this->line('line'); $this->info('info'); $this->comment('comment'); $this->question('question'); $this->error('error'); $this->warn('warn'); $this->table(['h1', 'h2'], [[1, 2]]); // 出力レベルの設定 $this->info('quiet', OutputInterface::VERBOSITY_QUIET); $this->info('normal', OutputInterface::VERBOSITY_NORMAL); $this->info('verbose', OutputInterface::VERBOSITY_VERBOSE); $this->info('very_verbose', OutputInterface::VERBOSITY_VERY_VERBOSE); $this->info('debug', OutputInterface::VERBOSITY_DEBUG); }
出力レベルはLaravelのLog機能同様に通常で出力、詳細まで出す、デバッグで出す、みたいなレベル付けで出力ができる
実行例
それぞれ出力されるものが異なってくる
php artisan output:test --quiet php artisan output:test php artisan output:test -v php artisan output:test -vv php artisan output:test -vvv
8-1-5 Commandの実行
コンソールのコマンドで実行させるだけでなく、プログラムを直接書いてCommandの実行も可能
ここでは例としてroutes/web.php に書いて Log::debug(); で出力してみる
<?php Route::get('/no_args', function () { Artisan::call('output:test'); }); // php artisan hello:option --param=1 --param=2 Route::get('/with_args', function () { Artisan::call('hello:option', [ 'arg' => 'value', '--switch' => 'false', ]); });
Artisan::call('コマンド', ['引数指定']);
Artisanというファサードで call メソッドを呼ぶ、
第一引数は呼びたいコマンドを指定
第二引数にコールバック関数を入れる
引数を指定可能、上の例では第二引数を配列にして2つの引数を送っている
ブラウザで設定したurlにアクセスすると
http://study_command.test/no_args
<?php public function handle() { $param = $this->option('param'); $this->comment('Hello Option param => ' . ($param ? '有' : '無')); log::debug(print_r('command HelloOptionParamCommand run!!',1)); }
handle() に書いたlogが出力されているのが確認できる
laravel.log
[2019-09-08 11:43:58] local.DEBUG: command output:test run!!
ちなみに handle() の中にcallメソッドで別のコマンドを呼ぶ処理を書けばCommandから別のCommandを実行することができる
<?php public function handle() { $this->call('hello:param'); }
8-2 Commandの実装
ここでは実際のコマンドラインアプリケーションの実装方法の解説をExportOrdersCommand
で説明する
8-2-1 サンプル実装の仕様
ここではコマンド実行でDBの値をTSV形式で出力できる機能を実装する
フルスペル:Tab Separated Values 読み方:ティーエスブイ 別名:タブ区切り TSVとは、文字や文字列の間にタブ記号を挿入して区切りを設けること、あるいは、そのようにして各データを区切って管理するファイル形式のことである。
書籍にはDB定義のカラム仕様や出力されるデータの形式が詳細に記載されているがここでは割愛する。
<?php // ひとまずseederファイルの一部でDB構造はなんとなく把握してください。 Schema::create('orders', function (Blueprint $table) { $table->increments('id'); $table->string('order_code', 32); $table->dateTime('order_date'); $table->string('customer_name', 100); $table->string('customer_email', 255); $table->string('destination_name', 100); $table->string('destination_zip', 10); $table->string('destination_prefecture', 10); $table->string('destination_address', 100); $table->string('destination_tel', 20); $table->integer('total_quantity'); $table->integer('total_price'); $table->timestamps(); $table->unique('order_code'); $table->index('order_date'); }); Schema::create('order_details', function (Blueprint $table) { $table->string('order_code', 32); $table->integer('detail_no'); $table->string('item_name', 100); $table->integer('item_price'); $table->integer('quantity'); $table->integer('subtotal_price'); $table->primary(['order_code', 'detail_no']); $table->index('order_code'); /** @noinspection PhpUndefinedMethodInspection */ $table->foreign('order_code')->references('order_code')->on('orders'); });
登録されるコマンドは以下の様なものとなる
vagrant@homestead:~/study_command$ php artisan list Laravel Framework 6.0.1 ### 省略 ### app app:export-orders 購入情報を出力する ### 省略 ###
出力される内容は以下の様なタブ区切りでカラム名とレコードを出力する
php artisan app:export-orders 購入コード 購入日時 明細番号 商品名 商品価格 購入点数小計金額 合計点数 合計金額 購入者氏名 購入者メールアドレス 送付先氏名 送付先郵便番号 送付先都道府県 送付先住所 送付先電話番号 1111-1111-1111-1111 2019-09-08 00:00:00 1 商品1 1000 1 1000 大阪 太郎 osaka@example.com 送付先 太郎 1234567 大阪府 送付先住所1 06-0000-0000
8-2-2 Commandの生成
8-1章でのコマンドに習いファイルを作成し、ひな形として動くコマンドファイルを作成する
php arrisan make:command ExportOrdersCommand
app\Console\Commands\ExportOrdersCommand.php
<?php namespace App\Console\Commands; use Illuminate\Console\Command; class ExportOrdersCommand extends Command { // コメントは省略 protected $signature = 'app:export-orders'; protected $description = '購入情報を出力する'; public function __construct() { parent::__construct(); } public function handle() { $this->info('hello'); } }
実行してみてhelloを確認
vagrant@homestead:~/study_command$ php artisan app:export-orders hello
8-2-3 ユースケースクラスとサービスクラスの分離
ここでは設計と実装について説明しているがこのUML図とコメントでおおよそを把握してください。
処理毎にクラスを分離して役割を明確にして、定められた役割のみを担うように実装をする。
これにより処理の再利用性が高まる。 8-3 では実際に ExportOrdersServiceクラスを再利用する。
また、テストを容易にでくるメリットもあり、図の真ん中の ExportOrderUseCaseも単体でテスト可能となる。
8-2-4 ユースケースクラスのひな形を作成する
まずごく簡単な処理の流れがつかめる実装を行う
Commandクラス(1階層目)に ExportOrdersUseCase
を注入して run メソッドに引数を入れて走らせる処理を書く
<?php namespace App\Console\Commands; use App\UseCases\ExportOrdersUseCase; use Carbon\Carbon; use Illuminate\Console\Command; class ExportOrdersCommand extends Command { protected $signature = 'app:export-orders'; /** @var TemplateExportOrdersUseCase */ private $useCase; protected $description = '購入情報を出力する'; public function __construct(ExportOrdersUseCase $useCase) { parent::__construct(); $this->useCase = $useCase; } public function handle() { $tsv = $this->useCase->run(Carbon::today()); echo $tsv; } }
ExportOrdersUseCase
クラス(2階層目)には runメソッドを書いて渡されたCarbonオブジェクトで Y-m-d形式の表示を行う仮実装を行う。
<?php declare(strict_types=1); namespace App\UseCases; use App\Services\ExportOrdersService; use Carbon\Carbon; final class TemplateExportOrdersUseCase { /** * @param Carbon $targetDate * @return string */ public function run(Carbon $targetDate): string { return $targetDate->format('Y-m-d') . 'の購入情報'; } }
コマンド実行で 本日 + 文字列 が出力されるようになる。
php artisan app:export-orders 2019-09-09の購入情報
8-2-5 サービスクラスの実装
次に3階層目(Service)の処理を書く
<?php declare(strict_types=1); namespace App\Services; use Carbon\Carbon; use Generator; use Illuminate\Database\Connection; final class ExportOrdersService { /** @var Connection */ private $connection; public function __construct(Connection $connection) { $this->connection = $connection; } /** * 対象日の購入情報を取得 * * @param Carbon $date * @return Generator */ public function findOrders(Carbon $date): Generator { return $this->connection ->table('orders') ->join('order_details', 'orders.order_code', '=', 'order_details.order_code') ->select([ 'orders.order_code', 'orders.order_date', 'orders.total_price', 'orders.total_quantity', 'orders.customer_name', 'orders.customer_email', 'orders.destination_name', 'orders.destination_zip', 'orders.destination_prefecture', 'orders.destination_address', 'orders.destination_tel', 'order_details.*', ]) ->where('order_date', '>=', $date->toDateString()) ->where('order_date', '<', $date->copy()->addDay()->toDateString()) ->orderBy('order_date') ->orderBy('orders.id') ->orderBy('order_details.detail_no') ->cursor(); // ここがポイント! cursor は呼び出し時点でレコードの読み込みはしない // https://localdisk.hatenablog.com/entry/2016/07/20/173208 // cursor メソッドを使っています。これは 5.2.33 から追加されました*1。 // 内部でジェネレータを使っているのでメモリ不足で死ななくて最高です。 } }
クエリビルダで単純に必要なデータを取得しているのみ。 引数にCoerbonの日付を入れてクエリ条件にしている。
ポイントは最後の cursor()
メソッドでこれは呼び出し時点でクエリを実行しないので、メモリ的に厳しい処理にも対応できるらしい。
コードの詳細な説明は書籍を参照のこと。
8-2-6 ユースケースクラスの実装
次に2階層目(UseCase)クラスを実装する。
<?php declare(strict_types=1); namespace App\UseCases; use App\Services\ExportOrdersService; use Carbon\Carbon; final class ExportOrdersUseCase { /** @var ExportOrdersService */ private $service; public function __construct(ExportOrdersService $service) { $this->service = $service; } /** * @param Carbon $targetDate * @return string */ public function run(Carbon $targetDate): string { // (1) データベースから購入情報を取得 $orders = $this->service->findOrders($targetDate); // (2) TSV ファイル用コレクションを生成 $tsv = collect(); // (3) タイトル行を追加 $tsv->push($this->title()); // (4) 購入情報を追加 foreach ($orders as $order) { $tsv->push([ $order->order_code, $order->order_date, $order->detail_no, $order->item_name, $order->item_price, $order->quantity, $order->subtotal_price, $order->customer_name, $order->customer_email, $order->destination_name, $order->destination_zip, $order->destination_prefecture, $order->destination_address, $order->destination_tel, ]); } // (5) 各要素を TSV 形式に変換 return $tsv->map(function (array $values) { return implode("\t", $values); })->implode("\n") . "\n"; } private function title(): array { return [ '購入コード', '購入日時', '明細番号', '商品名', '商品価格', '購入点数', '小計金額', '合計点数', '合計金額', '購入者氏名', '購入者メールアドレス', '送付先氏名', '送付先郵便番号', '送付先都道府県', '送付先住所', '送付先電話番号', ]; } }
書籍にはないが、そろそろSeederを追加してダミーデータを入れておく ファイルを書くより書籍用のGithubリポジトリのコードを流用するのが良い。
php artisan migrate php artisan db:seed
seeder に購入日を本日に指定したレコードを挿入しておく( Carbon::now()でよろしくやる )
作ったコマンド実行をすると、見出しとレコードがタブスペース繋がりで出力される
php artisan app:export-orders 購入コード 購入日時 明細番号 商品名 商品価格 購入点数小計金額 合計点数 合計金額 購入者氏名 購入者メールアドレス 送付先氏名 送付先郵便番号 送付先都道府県 送付先住所 送付先電話番号 1111-1111-1111-1111 2019-09-08 00:00:00 1 商品1 1000 1 1000 大阪 太郎 osaka@example.com 送付先 太郎 1234567 大阪府 送付先住所1 06-0000-0000
データが無い場合は見出しのみ出力される
8-2-7 Commandクラスの仕上げ
日付を引数で指定して渡してやるとその日に購入されたレコードが表示されるようになる
<?php ### 省略 ### protected $signature = 'app:export-orders {date}'; ### 省略 ### public function handle() { // 日付のパラメーターを取得してUseCaseに処理を渡す $date = $this->argument('date'); $tagetDate = Carbon::createFromFormat('Ymd', $date); $tsv = $this->useCase->run($tagetDate); echo $tsv; } ### 省略 ###
出力結果
vagrant@homestead:~/study_command$ php artisan app:export-orders 20180629 購入コード 購入日時 明細番号 商品名 商品価格 購入点数 小計金額 合計点数 合計金額 購入者氏名 購入者メールアドレス 送付先氏名 送付先郵便番号 送付先都道府県 送付先住所 送付先電話番号 1111-1111-1111-1112 2018-06-29 23:59:59 1 商品1 1000 2 2000 神戸 花子 kobe@example.com 送付先 太郎 1234567 兵庫県 送付先住所2 078-0000-0000 1111-1111-1111-1112 2018-06-29 23:59:59 2 商品2 500 1 500 神戸 花子 kobe@example.com 送付先 太郎 1234567 兵庫県 送付先住所2 078-0000-0000
vagrant@homestead:~/study_command$ php artisan app:export-orders 20190908 購入コード 購入日時 明細番号 商品名 商品価格 購入点数 小計金額 合計点数 合計金額 購入者氏名 購入者メールアドレス 送付先氏名 送付先郵便番号 送付先都道府県 送付先住所 送付先電話番号 1111-1111-1111-1111 2019-09-08 00:00:00 1 商品1 1000 1 1000 大阪 太郎 osaka@example.com 送付先 太郎 1234567 大阪府 送付先住所1 06-0000-0000
さらに必須引数に日時 オプションに出力先を指定すれば、その場所にファイルが出力される
<?php // オプションの引数を追加 protected $signature = 'app:export-orders {date} {--output=}'; public function handle() { $date = $this->argument('date'); $tagetDate = Carbon::createFromFormat('Ymd', $date); $tsv = $this->useCase->run($tagetDate); // オプションの引数を取得できれば指定されたpathに出力 $outputFilePath = $this->option('output'); if (is_null($outputFilePath)) { echo $tsv; return; } file_put_contents($outputFilePath, $tsv); }
出力pathとファイル名をオプションで指定すればファイルとして出力できる。
php artisan app:export-orders 20190908 --output tmp/orders.tsv