【輪読会資料】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
【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 4章 後半資料 レスポンス ミドルウェア
以下の記事は2019年8月8日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 HTTPリクエストとレスポンスの輪読会資料の一部となります。
今回は 4章後半 4-3『レスポンス』, 4-4『ミドルウェア』部分の記事をアップします。
また、元になっている書籍は以下となります。
PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応
- 作者: 竹澤有貴,栗生和明,新原雅司,大村創太郎,丸山弘詩
- 出版社/メーカー: ソシム
- 発売日: 2018/09/26
- メディア: 単行本
- この商品を含むブログを見る
また書籍のコードが記載されているGitHubレポジトリは以下となります。とても助かります!
GitHub - laravel-socym/chapter04-2: 4章 リクエスト・レスポンス
4-3 レスポンス
4-3-1 さまざまなレスポンス
Responseファサードの実態は Illuminate\Contracts\Routing\ResponseFactory
クラスである。
ファクトリークラスなので呼び出す生成メソッドで実際に生成されているResponseクラスは異なる。
アプリケーションからユーザーに返却するデータの種類でうまく使い分けを。
ここではデータタイプごとの返却方法を紹介する
文字列返却
シンプルなパターン、ダイレクトに下記例 hello world みたいに与えるか
Response メソッドの第三引数に 'content-type' => 'text/plain'
を指定する
chapter04-2/PlainTextAction.php at master · laravel-socym/chapter04-2 · GitHub
上記サイトよりコード引用
<?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; // 同一クラス名となりますので、このサンプルではファサードを別名としています use Illuminate\Support\Facades\Response as LaravelResponse; use function response; class PlainTextAction { public function __invoke(Request $request): Response { // 同一クラス名となりますので、このサンプルではファサードを別名としています $response = LaravelResponse::make('hello world'); // ヘルパー関数を利用する場合 $response = response('hello world'); // content-typeを変更 $response = response('hello world', Response::HTTP_OK, [ 'content-type' => 'text/plain' ]); return $response; } }
テンプレート出力
標準のBladeテンプレート view を使う場合等、 View ファサードを使う。responseヘルパーメソッド等、まあ普通に勉強していると最初の当たり前として出る奴。
chapter04-2/BladeAction.php at master · laravel-socym/chapter04-2 · GitHub
<?php $response = Response::view('view.file'); // 上記のメソッドと同じ結果が得られます $response = view('view.file'); // ステータスコードを変更し、ビューを出力します。 $response = response(view('view.file'), Response::HTTP_ACCEPTED);
JSON出力
chapter04-2/JsonAction.php at master · laravel-socym/chapter04-2 · GitHub
<?php $response = Response::make('hello world'); // ヘルパー関数を利用する場合 $response = response('hello world'); // content-typeを変更 $response = response('hello world', Response::HTTP_OK, [ 'content-type' => 'text/plain' ]);
任意のメディアタイプ
return response()->json(['message' => 'laravel'], Response::HTTP_OK, [ 'content-type' => 'application/vnd.laravel-api+json' ]);
また、jsonp を使いたい場合は 上記のメソッド名の json
を jsonp
に変えるだけ
ダウンロードレスポンス
ダウンローダー等DLをレスポンスさせたい場合は downloadメソッドを使う
chapter04-2/DownloadAction.php at master · laravel-socym/chapter04-2 · GitHub
<?php $response = Response::download(storage_path('app/cover_min.png')); // ヘルパー関数を利用する場合 $response = response()->download(storage_path('app/cover_min.png')); // 第二引数、第三引数に任意の指定を行うパターン $response = response()->download(storage_path('app/cover_min.png'), 'image.png', [ 'content-type' => 'image/png', ]); return $response;
リダイレクトレスポンス
リダイレクト先を指定する奴
HTTPパラメーターを渡したい場合は withInputメソッド
リダイレクト→エラーメッセージ等の場合は with メソッドを使う
<?php // どれも同じ結果なのでお好みで $response = Response::redirectTo('/'); $response = response()->redirectTo('/'); $response = ridirect('/') // リダイレクト時に動作を行う例 $response = ridirect('/') ->withInput($request->all()) ->with('error', 'Validation error!!'); return $response
Server-Sent Events 実装
Server-Sent Events(SSE)はhtml5からの機能、サーバー側からのプッシュデータ通信を利用できるがWebSocketと異なり、双方向通信は出来ない。
chapter04-2/StreamAction.php at master · laravel-socym/chapter04-2 · GitHub
書籍だと以下の様に response()->stream( ... )
メソッドを使っているが、
現在のLaravelでは? response()->StreamedResponse( ... )
が使える様だ
LaravelでSSE(Server Sent Events)を利用してサーバから通知する | Minory
また、上記の記事でSSEに加えてクライアント側から非同期通信でステータス情報を送る事で、疑似的な双方向通信を行う事ができるそうです。
<?php namespace App\Http\Controllers; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\StreamedResponse; class StreamAction { /** * @return StreamedResponse */ public function __invoke(): StreamedResponse { return response()->stream(function () { while (true) { echo 'data: ' . rand(1, 100) . "\n\n"; ob_flush(); flush(); usleep(200000); } }, Response::HTTP_OK, [ 'content-type' => 'text/event-stream', 'X-Accel-Buffering' => 'no', 'Cache-Control' => 'no-cache', ]); } }
4-3-2 リソースクラスを組み合わせた REST API アプリケーション
HATEOASとは
Hypermedia as the Engine of Application State の略。
APIのレスポンスにも別リソースのリンクを埋め込み、リンクを辿るだけで別アプリケーションの呼び出しを可能にすること。らしい。
ブログで考えると、記事の出力をするAPIにユーザープロフィールや個別のコメントへ投稿者のプロフィールの情報へのURLリンクを埋め込む事で、通常、URLのpathやページの仕様や変化した場合、完全分離されているクライアント(フロント側)ではそれを知る術はないが、リンクが埋め込まれている事で、これらの問題を解決できる。という仕組みと思想的なもののようです。
この考えに対応しているJSONフォーマットとして、[JSON API], [HAL4], [JSON-LD] 等がある
Web APIにはJSONベースのフォーマットを使おう - Qiita
以下は HAL を採用した例となる
引用元
The Hypertext Application Language
{ "_links": { "self": { "href": "/orders" }, "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }], "next": { "href": "/orders?page=2" }, "ea:find": { "href": "/orders{?id}", "templated": true }, "ea:admin": [{ "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" }] }, "currentlyProcessing": 14, "shippedToday": 20, "_embedded": { "ea:order": [{ "_links": { "self": { "href": "/orders/123" }, "ea:basket": { "href": "/baskets/98712" }, "ea:customer": { "href": "/customers/7809" } }, "total": 30.00, "currency": "USD", "status": "shipped" }, { "_links": { "self": { "href": "/orders/124" }, "ea:basket": { "href": "/baskets/97213" }, "ea:customer": { "href": "/customers/12369" } }, "total": 20.00, "currency": "USD", "status": "processing" }] } }
どのようなJSONフォーマットを採用するかは決まっておらす、仕様は様々だが、
Laravel ではこれらのサポート機能として API Resource
機能が提供されている
REST & Hypermedia APIs - Alpha Hydrae
リソースクラスの基本
resource ってのを make できるらしい
以下のコマンドでapp/Http/Resource
ディレクトリ配下にファイルが作成される
php arrisan make:resource UserResource php arrisan make:resource CommentResource php arrisan make:resource CommentResourceCollection php arrisan make:resource ArticleResource
ここではeroquent を使っているが、そうじゃなくてもOKだそうです。
作成した4つのファイルを使って、上の例にある、HATEOASのJSON を出力する例を以下に示します。
chapter04-2/ArticlePayloadAction.php at master · laravel-socym/chapter04-2 · GitHub
chapter04-2/ArticleResource.php at master · laravel-socym/chapter04-2 · GitHub
chapter04-2/UserResource.php at master · laravel-socym/chapter04-2 · GitHub
chapter04-2/CommentResource.php at master · laravel-socym/chapter04-2 · GitHub
chapter04-2/CommentResourceCollection.php at master · laravel-socym/chapter04-2 · GitHub
ルーター登録
<?php Route::get('/payload', 'ArticlePayLoadAction');
4-4 ミドルウェア
Laravelにおける、ミドルウェアはコントローラークラスの前後に位置し主にHTTPリクエストのフィルタリングやHTTPレスポンスの変更を行う
4-4-1 ミドルウェアの基本
Laravelのミドルウェアは以下の3種類がある
この流れはフィルタリングなどで使用
HTTPリクエスト ---> ミドルウェア ---> コントローラー
この流れはレスポンス内容の変更や新たなレスポンスの生成が可能
HTTPリクエスト <--- ミドルウェア <--- コントローラー
4-4-2 デフォルトで用意されているミドルウェア
app/Http/kernel.php
の App\Http\Kernel
クラスで定義されている
グローバルミドルウェア
ルーターに登録されたコントローラークラスが捜査する前に実行される
ルートミドルウェア
デフォルトでwebミドルウェアグループに記述されている
名前付きミドルウェア
ルーターへの登録またはコントローラークラスのコンストラクタなどで任意の名前を指定して利用する
Laravel 標準では多数のミドルウェアが登録されてすでに動作しているが、不要なミドルウェアを削除するとパフォーマンス向上が見込めるケースがあるので必要に応じて使うとよいらしい。
4-4-3 独自ミドルウェアの実装
アプリ固有のミドルウェアを利用するには専用ミドルウェアを実装する必要がある。
ここではリクエストヘッダとレスポンスヘッダにログを書き出すミドルウェア実装を例に解説する。
ミドルウェアクラスの生成
実装例として HeaderDumper
クラスを作成する
ミドルウェア作成コマンド
php aritsan male:middleware HeaderDumper
すると app/Http/Middelware
ディレクトリ配下に HeaderDumper.php
が作られる
リクエストヘッダのログ出力
このクラスでリクエストヘッダをログに書き出す処理の実装例が以下
引用元 : chapter04-2/HeaderDumper.php at master · laravel-socym/chapter04-2 · GitHub
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Response; class HeaderDumper { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function handle($request, Closure $next) { // リクエストheader を log 出力する if ($request instanceof Request) { $this->logger->info('request', [ 'header' => strval($request->headers) ]); } // 上記同様の結果となるヘルパ関数を使った例 // info('request', ['header' => strval($request->headers)]); /** @var Response $response */ $response = $next($request); // レスポンスheaderをログ出力する if ($response instanceof Response) { $this->logger->info('response', [ 'header' => strval($response->headers) ]); } return $response; } }
レスポンスヘッダのログ出力
上のコードにある後半部分がレスポンスヘッダをログ出力する部分、以下にも記述
レスポンスヘッダを取得するには、ミドルウェアクラス内の $next($request)
の戻り値を取得する。
<?php /** @var Response $response */ $response = $next($request); // レスポンスheaderをログ出力する if ($response instanceof Response) { $this->logger->info('response', [ 'header' => strval($response->headers) ]); } return $response;
ミドルウェアの登録
ミドルウェアを作ったら App\Http\Kernel クラスに登録が必要
<?php namespace App\Http; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel { protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\UrlPath::class, \App\Http\Middleware\CssFileDate::class, // ここに登録しないと動きません seeder の run() とかと同じイメージですね \App\Http\Middleware\HeaderDumper::class, // laravel本 4-4 サンプル ]; protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, // 省略 ], 'api' => [ 'throttle:60,1', 'bindings', ], ]; protected $routeMiddleware = [ // routes/web routes/api で使えるアレはココで定義している 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, // 省略 ]; }
routeMiddeware は routes/web.php や routes/api.php のルーティングで使えるアレを登録できる
<?php Route::get('admin/profile', function () { // $routeMiddleware の auth は下のこれっすね })->middleware('auth');
おまけ オレオレミドルウェア
以下を参考に作ったものです
キャッシュを有効にしつつ、cssやjsファイルの変更を確実に反映させる – doop
css ファイルって更新してもブラウザ側でキャッシュが読み込まれてしまうケースがあって、開発中に更新してもデザイン変わらず「うぎゃー」ってなることがある。しかし css 読み込みの際にクエリパラメーターを付けて、毎回値を変えれば再読み込みされます。
<link href="http://app_whois.test/css/livelynk.css?q=ここにランダムな値" rel="stylesheet">
しかし、適当なランダム値にすると毎回css呼ばれるのも本番では遅くなって嫌なので、cssファイルの更新日時をクエリのパラメーターにして読み込んでやれば、cssを更新した時だけ、再読み込みされる!というちょっとうれしい奴です。
<link href="http://app_whois.test/css/livelynk.css?q=20190628113106" rel="stylesheet">
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\View; class CssFileDate { public function handle($request, Closure $next) { // ファイルpathから更新日を出力、pathにパラメータを入れcssや画像のキャッシュクリアに利用 $path = public_path(); $filename = $path . '/css/livelynk.css'; if (file_exists($filename)) { $file_date = date('YmdHis', filemtime($filename)); } else { $file_date = str_random(6); } // file_date という変数が view のどこでも使える様になる View::share('file_date', $file_date); return $next($request); } }
bladeテンプレート側ではこんな風に使えます
<link href="{{ asset('css/livelynk.css') }}?q={{$file_date}}" rel="stylesheet">
Laravelの単機能を作るまでの大まかな流れ
設計やら命名規則やら一緒くたになってごちゃごちゃとしてます。
また、俺ルールと一般的なルールと、厳密なルールがごっちゃにかいてあります。
が、自分なりに他の人と共通作業を行う際のコンセンサスを取るメモみたいなものをアップしておきます。何かの参考になれば幸い。
(アプリケーションはAPIとし、ごく基本的な設定関連はほぼ終わっている状態と想定)
ルーティング定義をする
routes/api.php
ルートプレフィックスで上層のpathをまとめる
基本アルファベット順で並べる
特別な処理は上か下の行にまとめる
同じControllerの処理はまずCRUDの順で並べ、次にその他の処理を並べる。
基本はControllerで書いたメソッドの順と揃っているのが望ましい
しかし、これだと1行で行けるらしい、他のメソッド入れた際はどうするんだろう?
Route::resource('users', 'UserController');
Controller 作成
コマンド例 :
php artisan make:controller HogeFugaController --resource
( --resources オプション CRUDメソッドを自動生成してくれる)
ディレクトリ/命名例 :
app/Http/Controllers/HogeFugaController.php
命名規則:
- 単数形,table名か機能名の UperCamelCase
- 最後に Controller を書く
アノテーションにAPI仕様を書く
requestを受けてserviceに送り返り値をもらう処理を書く
retrun [ ] で想定するAPIの返り値を仮作成
フロント側に要件での最低限の出力を可能とする
unit testファイルを作る
ひとまず pathを指定して
->assertStatus(200) のテストを書く
テストを実施する(時間があればこの辺詳細書きたい)
migration ファイル作成
コマンド例 :
php artisan make:migration create_users_statuses_table
ディレクトリ例 :
app/Http/Controllers/HogeFugaController.php
命名規則 :
- スネークケース
- table(複数形)名に続けて create|edit|delete のいずれかを書く
- 最後に
table
を書く
1テーブルの作成・編集・削除につき1ファイルを生成する
migration実施とphpMyAdminでの仕様との差を確認
- comment->('仮カラム、仮型') 等と通常のコメントで仮である事を明文化しておく
- コメントアウトをして作らないでおく
factory 作成
migrationファイルと合わせて factoryを作る。
コマンド例 :
php artisan make:factory HogeFugaFactory
ディレクトリ/命名例 :
database/factories/HogeFugaFactory.php
faker を使ったり、Corbon や決め打ちの値で1レコード文の標準的なダミーレコード作成を定義する
seeder ファイル作成
コマンド例 : php artisan make:seeder UserStatusTableSeeder
ディレクトリ/命名例 : database/seeds/UserStatusTableSeeder.php
ダミーレコードを作る為の定義をするファイル
up メソッド内で未定義のカラムがある場合は faker で定義された値でレコードが生成される
upメソッド内の以下の違いに注意、作成時と編集時はメソッド名が異なる
Schema::create( ... )
Schema::table( ... )
忘れがち!
database/seeds/DatabaseSeeder.php
に
$this->call(UserStatusTableSeeder::class);
を書かないとseedingは実行されない
また、Seeder書いたら以下のコマンドもやっておく
composer dump-autoload
少なくともレコード1件目はid系カラムのリレーションが成立する現実的値で具体的なデータを入れておく
2件目以降必要な現実的なデータがあれば、個別に生成できるようにしておく
以降のデータは他のtableの連携等含め、リレーションされるidは固定値、その他の値はある程度ランダムで生成されるレコード数を現実的な運用時のページング等の移動が行える程度には入れておく
seeder実施、各カラムに正しくfakeの値が入っているか確認
モデル作成と設定
コマンド例 :
php artisan make:model Models/HogeFuga
ディレクトリ/命名例 :
app/Models/HogeFuga.php
※コマンド時はフォルダpathの指定を間違えないこと
命名規則:
- 単数形,table名か機能名の UperCamelCase
- tableの場合は対となるControllerを持つ
- 最後に Model は書かない
hidden 設定
filiter 設定
table 名指定
リレーション設定
ここにクエリビルダはEloquentの処理は書かない
あとでやる 例外として scpope 等の共通のSQLフィルタ処理のメソッド等を書く
Service 層の作成
app/Http/Services/HogeFugaService.php
※ artisanコマンドでは作れない、コピペで作る
命名規則:
- Controllerと対にする
- 単数形,table名か機能名の UperCamelCase
- 最後に Service を書く
- Controller が
HogeFugaController
なら - Service は
HogeFUgaService
となる
- Controller が
必要なら Controllerから受けたrequestを受ける
1行で済むクエリビルダやEloquentならここに書く
必要なビジネスロジック処理を書く
複数行に渡るDB処理は、 Repositoryに渡して返り値をもらう処理を書く 引数を渡してRepository処理に反映させても良い
必要であれば他のtable層のRepository層を呼び出しても良いがなるべくしない
Repository 層の作成
app/Http/Repositorys/HogeFugaRepository.php
※ artisanコマンドでは作れない、コピペで作る
命名規則:
- 単数形,table名か機能名の UperCamelCase
- 最後に Repository を書く
- Serviceと対にする
- Controller が
HogeFugaController
で - Service が
HogeFUgaService
なら - Repository は
HogeFugaRepository
となる
- Controller が
use App/Models/Hoge
use DB
を書く
DBから値を拾う処理のみを書く
簡単な if分岐やswitch 等はOK
メソッドにどのServiceのメソッドからの呼び出しかコメントを書く
引数を渡して処理を変える等しても良いが、詳細やルールをコメントする
Requests (フォームリクエスト)によるバリデートの作成
コマンド例 : php artisan make:request StoreBlogPost
ディレクトリ/命名例 : app/Htp/Requests/HogeFugaCreareUpdate.php
命名規則:
- 単数形,table名か機能名の UperCamelCase Controller と同じ
- 続けて対となるControllerのメソッド名
- create, update 等、複数のメソッドで共通化できるものは両方の名前を続けて書く
authorrize()
は、ほぼ true となる
rules()
にバリデート条件を書く、仕様書に合わせて書く
特殊なバリデートが必要な箇所はオリジナルバリデートを作成する
本来はこのへんでしっかり test を書いて検証をしたい所だが、ひとまず、値を入れてみて手動のpost,get,put,delete,のtestをする
ひとまずこんな感じ。
vagrant Homestead でホストOSの共有フォルダが見れなくなった際の対処
状況
ある日Homesteadの vagrant up
時に以下の様なエラーが出てゲストOSとホストOSフォルダ共有が出来なくなってしまった。
(windows10 64bit環境)
Going on, assuming VBoxService is correct... bash: line 5: setup: command not found ==> homestead-7: Checking for guest additions in VM... The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed! setup Stdout from the command: Stderr from the command: bash: line 5: setup: command not found
エラーでググると以下のページに行き当たり同じ対処をしたが、 GuestAdditions の インストールをHomestead側で行ってもmountがされない様だった。
対応したが私の環境では解決しなかった方法
上記のページに習いmountしてみたが上手くいかなかった。
vagrant@homestead:/mnt$ sudo ./VBoxLinuxAdditions.run Verifying archive integrity... All good. # ... インストール中のメッセージ... VirtualBox Guest Additions: Running kernel modules will not be replaced until the system is restarted vagrant@homestead:/mnt$ cd vagrant@homestead:~$ ls /etc/init.d/ | grep vboxadd # 何も出ない! 入ってない! vagrant@homestead:~$ ls /etc/init.d/ acpid avahi-daemon cron ebtables iscsid lvm2-lvmetad mdadm nfs-common open-vm-tools php7.2-fpm postgresql rsync supervisor unattended-upgrades apparmor beanstalkd cryptdisks grub-common keyboard-setup.sh lvm2-lvmpolld mdadm-waitidle nginx php5.6-fpm plymouth procps rsyslog sysstat uuidd apport blackfire-agent cryptdisks-early hwclock.sh kmod lxcfs memcached ntp php7.0-fpm plymouth-log redis-server screen-cleanup udev x11-common atd console-setup.sh dbus irqbalance lvm2 lxd mysql open-iscsi php7.1-fpm postfix rpcbind ssh ufw # やはり vboxadd がいない!
ちなみにこの記事執筆時のバージョンは以下 6.0.8 でした。 Index of /virtualbox/6.0.8
wget http://download.virtualbox.org/virtualbox/6.0.8/VBoxGuestAdditions_6.0.8.iso
そこでHomesteadのBOXファイル自体が古く対応できていないのでは?と思い至りupdateすることにした
windows側 の Gitbash で以下のコマンドを実行
$ vagrant box list # その他のBOXファイルがここに表示されている # updateを重ね現在は 6.1.0 laravel/homestead (virtualbox, 5.0.1) laravel/homestead (virtualbox, 5.2.0) laravel/homestead (virtualbox, 6.1.0) $ vagrant box update --box laravel/homestead # ここでダウンロードとupdateが行われるがしばらく時間がかかる $ vagrant box list # その他のBOXファイルがここに表示されている laravel/homestead (virtualbox, 5.0.1) laravel/homestead (virtualbox, 5.2.0) laravel/homestead (virtualbox, 6.1.0) laravel/homestead (virtualbox, 8.0.0-beta) # インストール後再度確認、上記の様に8.0.0-betaが入った
しかし、無事updateされて晴れてvagrant up
したが駄目。再度 GuestAdditions をmountしても同じ結果となる。乗らない…。
問題点が判明
windows側のGit bash で状況確認中、以下のerrorが出た事に注目
$ vagrant vbguest vagrant vbguest [homestead-7] GuestAdditions seems to be installed (6.0.8) correctly, but not running. bash: line 5: setup: command not found The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed!
もう直接的に GuestAdditions 6.0.8が駄目っす!って言っている。犯人はお前か。
このerror文章で検索をかけてみた所、以下のページが見つかる。
リンク先引用
[devapp] GuestAdditions seems to be installed (6.0.6) correctly, but not running. Redirecting to /bin/systemctl start vboxadd.service Redirecting to /bin/systemctl start vboxadd-service.service bash: line 4: setup: command not found ==> devapp: Checking for guest additions in VM... The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed!
setup
Stdout from the command:
Stderr from the command:
bash: line 4: setup: command not found ```
6.0.6でも同じ問題があったようだ
で、以下のコメントに注目
alvaro-canepa commented on 20 Apr
I have the same issue with Homestead and Virtualbox 6.0.6.
Adding this to Vagrantfile solve the problem:
if Vagrant.has_plugin?("vagrant-vbguest") config.vbguest.auto_update = false end
alvaro-canepa さんが、Vagrantfileにこの設定を追加しろって言ってる。
このコードはつまり vagrant-vbguest
はアップデートさせず使えって設定を書けってことらしい。
結論
という事で Homestead内の Vagrantfile を開き if文の中に設定を追記
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # ここには他の設定諸々が書かれている # 2019/06/24 add https://github.com/dotless-de/vagrant-vbguest/issues/333 if Vagrant.has_plugin?("vagrant-vbguest") config.vbguest.auto_update = false end end
これでvagrant reload --provision
した所、諸々の再設定がされて無事共有フォルダが反映されるようになりました。
でもいつかupodateが反映され、 その際には vagrant-vbguest も正しく修正された暁には、上の設定は消して新たな設定を反映されるようにするのが望ましいでしょう。
Apache2.4で複数ドメインを1つのサーバーで動かして Let's Encrypt でssl対応する方法のメモ
Apache2.4で複数のドメインを当てて、Let's Encryptで二つのサイトでssl対応をした際のメモを記しておきます。
- 要約
- Let's Encryptで複数のドメインで証明書を取るにはコマンドがあるが、既に一つのSSL認証を取得している場合は一度失効と削除をする必要があるようです。
/etc/http/conf/httpd.conf
の記述を/etc/httpd/conf.d/ssl.conf
に転載するのが基本- Let's Encryptで取得したキーのありかを conf ファイルで指定してやらないと駄目
- 何度も設定間違って再取得していると、1時間、または1週間のデッドロックを食らう
- ドメインのアクセスには
http://
https://
www.
有りwww.
無しの合計4パターンがあるが、今回はhttps://
のwww無しで統一リダイレクトをさせる設定にした
Let's Encryptで複数のドメインで証明書を取るコマンド
既に一つのSSL認証を取得している場合は一度失効と削除をする必要があるようです。 削除の方については以下のサイト等を参考にさせていただきました。
2つのサイトでのSSL認証の取得コマンド
rootユーザーになって以下のコマンドを打ちます。
例はドメイン aaa.site
, bbb.site
の二つとしています。
サイトの公開フォルダはサーバー内のそれぞれ /var/www/aaa-site
, /var/www/bbb-site
にあるものとします。
# certbot certonly --webroot -w /var/www/aaa-site -d aaa.site -w /var/www/bbb-site -d bbb.site
なお、インストールの経緯でコマンドの最初は certbot-auto
等にもなるようです。
次にApacheの設定ファイルです。
/etc/httpd/conf/httpd.conf
NameVirtualHost *:80 # 一つ目のサイトの設定 <VirtualHost *:80> DocumentRoot /var/www/aaa-web-site ServerName aaa.site # エイリアスの指定で www 付きでのアクセスも受け入れる事が出来るようです。 ServerAlias www.aaa.site AddDefaultCharset UTF-8 <Directory "/var/www/aaa-web-site/"> AllowOverride All # リダイレクト処理を行う設定 RewriteEngine On # 以下の二つのドメインでアクセスがあった場合書き換え処理を行う # http://aaa.site と http://www.aaa.site でアクセスがあった場合、リダイレクトをする RewriteCond %{SERVER_NAME} =aaa.site [OR] RewriteCond %{SERVER_NAME} =www.aaa.site # リダイレクト先は https://aaa.site となる、URIのパラメーターがある場合はそのままで飛ばす # 301リダイレクト LはマッチしたらRewriteを止め以降のルールは無視するそうです RewriteRule ^ https://aaa.site%{REQUEST_URI} [R=301,L] </Directory> </VirtualHost> # ふたつめのサイトの設定 やっている事は同じです <VirtualHost *:80> DocumentRoot /var/www/bbb-web-site ServerName bbb.site ServerAlias www.bbb.site AddDefaultCharset UTF-8 <Directory "/var/www/bbb-web-site/"> AllowOverride All RewriteEngine On RewriteCond %{SERVER_NAME} =bbb.site [OR] RewriteCond %{SERVER_NAME} =www.bbb.site RewriteRule ^ https://bbb.site%{REQUEST_URI} [R=301,L] </Directory> </VirtualHost>
/etc/httpd/conf.d/ssl.conf
かなり前に行ったLet's Encryptが自動生成した記述があるかは不明。長い記述の一部分のみを記載
NameVirtualHost *:443 # 一つ目のサイトの設定 <VirtualHost _default_:443> SSLEngine on DocumentRoot /var/www/aaa-web-site ServerName aaa.site ServerAlias www.aaa.site # SSL認証キーのありかを指定する Let's Encrypt で必要な設定 # ファイルの場所はCentOS7の私の環境の場合です。環境によって異なる可能性があります。 SSLCertificateFile /etc/letsencrypt/live/aaa.site/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/aaa.site/privkey.pem SSLCertificateChainFile /etc/letsencrypt/live/aaa.site/chain.pem <Directory "/var/www/aaa-web-site/"> AllowOverride All AddDefaultCharset UTF-8 RewriteEngine On # https://www.aaa.site でアクセスがあった場合のリダイレクト設定になる RewriteCond %{SERVER_NAME} =www.aaa.site # https://aaa.site へリダイレクトする、URIのパラメーターがある場合はそのままで飛ばす RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </Directory> </VirtualHost> # ふたつ目のサイトの設定 やってることは同じですが、 # 証明書キーの設定は1つめのサイトで行っているので必要無いのがミソです。 <VirtualHost _default_:443> SSLEngine on DocumentRoot /var/www/bbb-web-site ServerName bbb.site ServerAlias www.bbb.site <Directory "/var/www/bbb-web-site/"> AllowOverride All AddDefaultCharset UTF-8 RewriteEngine On RewriteCond %{SERVER_NAME} =www.bbb.site RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </Directory> </VirtualHost>
参考にさせていただいたサイト
近況報告
最近あんまり書いて無いですが、色々始まってます。
最近、講師から職業プログラマーにジョブチェンジを果たしました
5月の連休明けから、開発(PHP)の委託業務を週3日で始めました。通勤1.5時間がツライですが、お仕事をしつつえらい勉強になってます。
残りの2日は毎年やってる大学の非常勤講師(1年生にコンピュータの基礎を教える)ですが今年は色々あり過ぎて正直しんどい状況…。
現状の目標は8月から週5で通勤が辛くないPHP,Laravelのお仕事を探す事です。去年作ったwi-fiで滞在者がわかるアプリLivelynkを売るのではなく、あれを作った自分を売る、という風に視点を変えたら、40過ぎてからプログラミングを始めた人でも、けっこう高い価値が付いたみたいです。
滞在者確認アプリの現状
そのLivelynkの改修がなかなか進みませんが、ギークオフィス恵比寿ではインフラ的な位置を完全に確立して現在も絶賛稼働中です。 実はドメインの設定でやらかし連休中に数日サービスが繋がらない状態が続いたのですが、その際の不便な事といったら!みたいな感覚になったのが、申し訳なさもありますが、大事なものになっている事を認識出来た良い機会でもありました。
あの記事を書いた後から付いた機能があります。
一つはGoogleHomeとの連携、現在は人が来訪すると挨拶をさせている程度ですが、今後、価値ある情報を提供させることをするツモリです。
来訪者が来た際に挨拶するようになりました!他にも色々喋りますが、なんとしゃべるかは恵比寿訪問か、ご購入か、もしくは公開ソースコードでわかります pic.twitter.com/a0mOglBdkz
— sakamata (@sakamata) 2018年12月7日
Livelynkサーバーからラスパイを介して、GoogleHomeにしゃべらせるというハック的な実装の詳細はともかくですが、静かなオフィスで唐突にGoogleHomeが余計な挨拶や決まりきった挨拶をする際の気まずさといったら…。新たなソリューションでこれをやってはいかん!というのがわかっただけでも大きな収穫でした。
そしてもう一つはこの場所に『行く予定』を宣言できる機能です。 元々同じ仲間の兄弟アプリ『ツモリンク』
というのがありまして、任意の場所に今から行こうかな?というあいまいなニュアンスを宣言するアプリなのですが、この宣言ができる機能のみLivelynkに内包して実装しました。
実装の際は新機能の部分のみですが、習ったばかりのテスト駆動開発を意識して、テストを先に書いて実装をして、テストが通れば実装もOK、というフローで作りました。慣れずに時間はかかりましたが、今の所大きなバグは無い筈です。(つい昨日細かな不具合報告があったけど…)
あと、簡単な実装ですが、Googleカレンダーをメインの画面に表示させる様にして、近日中のギークオフィス恵比寿のミーティングやイベントが、一目でわかる様にしました。
また、Livelynkは企画や機能として何か重要なパーツが欠けている気がしているので、細かな機能追加をして使いやすさを追求しつつ、新しい価値を提供できる様なものにして行くツモリです。
その他お仕事や今後の事
また、小規模ですがプログラミングやウェブサイト制作のお仕事もいただいています。あと、現在週一で1時間だけ、プログラミング初心者の人に基礎を教えるというビデオチャットのアルバイトもしてます。という事でWord,やExcelだけでなくプログラムを人に教えられる所まで、ちょっとだけ来たみたいです。
でも、人に教える仕事ってものすごいやりがあって、自分も勉強になるのですが、時間ばかりかかってなかなか実入りが少ないのが悩み所ですね。
という事で、遠い目標として通貨や評価の新しい価値体系を作る為に、現在からの自分の価値を最大限発揮できるに様にするにはどうしたらいいのか?みたいな事を考え始めてます。
ゆるふわLaravel勉強会 (認証/JWT) 認証に関する資料
Laravel 認証についての色々まとめ
以下の記事は 2019/4/1 コワーキングスペース秋葉原Weeybleで行われる輪読会 [秋葉原] ゆるふわLaravel勉強会 (認証/JWT)のための認証に関する資料となります。
内容は以下の有志によるリファレンスサイトの記事の要約となります。 Laravel 5.8 認証
また、バージョンはLaravel 5.8.8 を前提にしています。
認証クイックスタート
Laravelインストール直後は認証系がフロント側で動く状態にはなっていないが、Controller等は既に準備されている
Controllers/Auth
配下
コントローラー | 用途 |
---|---|
RegisterController | 新ユーザーの登録 |
LoginController | 認証処理 |
ForgotPasswordController | パスワードリセットのためのメールリンク処理 |
ResetPasswordController | パスワードリセット処理 |
ひとまず認証付きのアプリを作るには、まずは以下のコマンドを打って、フロント側やルーティングに認証系の処理を自動生成させる
php artisan make:auth
コマンドを叩くとファイルに記述が追加されたり、新規ファイルが作られたりする
もし認証付きのアプリケーションを作るのであればfirst commit直後位に実施してしまうのが良い
変化のあるファイルの紹介
ルーティング
routes/web.php
// 以下2行が追加される
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Auth::routes();
で登録画面、ログイン画面、パスワードリセットのすべてのルーティングを設定してくれている、個別に編集が必要な場合は、この行を廃止して画面毎にルーティングを定義する。
HomeControllerの追加
app\Http\Controllers\HomeController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HomeController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Show the application dashboard. * * @return \Illuminate\Contracts\Support\Renderable */ public function index() { return view('home'); } }
ルーティングの2行目に追加された処理を行うControllerだが、ログイン直後の画面のサンプル例となる。 ログインすると/home
に移動するので、アプリケーションの仕様に従い、表示を作り込めば良いし、Home
という名前が気に食わないなら随意に変更する
その他以下の各viewファイルが自動生成されます。
resources\views\home.blade.php
resources\views\auth\login.blade.php
resources\views\auth\register.blade.php
resources\views\auth\verify.blade.php
resources\views\auth\passwords\email.blade.php
resources\views\auth\passwords\reset.blade.php
resources\views\layouts\app.blade.php
ログイン・登録・パスワードリマインダ等のページと機能も自動で生成してほぼ機能するようになります。
ブラウザでルートのpathにアクセスすると、画面左上に[LOGIN]と[REGISTER]のリンクが表示されるようになります。
認証の動作確認
まずはDBが無いので作ります。(vagrant環境でMySQLがある前提)
$ mysql -u root -p secret mysql> create database your_database_name; mysql>exit
migrateしてDBにtableを作ります。
$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table
user table以外にpassword resetのtableもcreateされました。
これでREGISTER出来る様になりました。
登録画面に移動して、登録すると、DBにuserが生成され、ログインして/home
にリダイレクトされます。
当然一度ログアウトしてログインができることも確認できます。
また、ログイン画面にパスワードを忘れた際のMailリマインダ―がありますが、mailの設定をしないと飛びませんので、今回は割愛
以上が、クイックスタートで作られた認証の初期概要です。
認証のカスタマイズ (初級編)
初期状態は以下の様な認証の仕様となっていますが、これは簡単に変更が可能です。
以下はリファレンスサイトの内容をほぼ転載しています。
ログイン後のリダイレクト先 /home
の変更
LoginController
、RegisterController
、ResetPasswordController
、VerificationController
のredirectTo
プロパティで、認証後のリダイレクト先の場所を定義してください。
protected $redirectTo = '/';
ログイン時のEmailをユニークな username, user_id,等に変更する
ログイン時はemailとpasswordの組み合わせが認証のデフォルトだが、emailをuser_id等に変更したい場合。
これをカスタマイズしたい場合は、
LoginController
でusername
メソッドを定義してください。
public function username() { return 'user_id'; }
当然user Tableをmigrateして変更したいユニークとなるカラムを追加してください。
登録済みユーザーのロール別制限 guard メソッド
webアプリを普通に作ってる場合は個人的にあまり使いませんが、APIやSPAの際にはよく使う事になるそうです。
LoginController
、RegisterController
、ResetPasswordController
でguard
メソッドを定義してください。メソッドからガードインスタンスを返してください。
use Illuminate\Support\Facades\Auth; protected function guard() { return Auth::guard('guard-name'); }
認証済みユーザーの取得 -Authファサード超便利-
Auth::user()->email
とかでuser関連のデータをControllerやviewですぐ取得できる。
基本は認証時に行った user Tableのカラムのデータが取得できるので、自分のデータを取得したい際にとても便利に使えます。
use Illuminate\Support\Facades\Auth;
Auth::user()->email; // taro@gmail.com
認証中のユーザーか調べる -必須並みの便利機能-
Auth::check()
これも 認証してる/してない を簡単に切り替え判断できる。controllerでもviewでも使える。認証の有無で処理や表示を変える際に便利!よく使う。
use Illuminate\Support\Facades\Auth; if (Auth::check()) { // ログイン中の場合の処理 } else { // 非ログイン中の処理 }
認証済みのみ通すページをルーティングで指定
この辺はルーティングの説明時にも紹介した内容で、ルーティングrouter/
でチェーンメソッド->middleware('auth')
と書くと、認証時のみ有効となるルーティングとして定義できます。
Route::get('profile', function() {
// 認証済みのユーザーのみが入れる
})->middleware('auth');
それ以外でも例えばコントローラのコンストラクタでもmiddlewareメソッドを呼べる
public function __construct() { $this->middleware('auth'); }
認証回数制限
brute-force対策が最初から出来ている感じ?
Laravelの組み込みLoginControllerクラスを使用している場合、
Illuminate\Foundation\Auth\ThrottlesLogins
トレイトが最初からコントローラで取り込まれています。デフォルトでは何度も正しくログインできなかった後、一分間ログインできなくなります。制限はユーザーの名前/メールアドレスとIPアドレスで限定されます。
自前のユーザー認証 (中級編)
リファレンスにある内容を紹介する
app\Http\Controllers\Auth\LoginController.php
に
authenticate
メソッドを新たに定義する。
認証系のカスタマイズはvendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php
にあるメソッドをオーバーライドするのが応用編の入り口の様です。
ちなみに下記のIlluminate\Foundation\Auth\AuthenticatesUsers.php
内のauthenticated
メソッドの実体は空メソッドで、カスタマイズ専用のメソッドである事がわかります。
/** * The user has been authenticated. * * @param \Illuminate\Http\Request $request * @param mixed $user * @return mixed */ protected function authenticated(Request $request, $user) { // }
これを以下の様にLoginController.php
に追加で記述をします。
以下、リファレンスサイトの例を転載します。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { /** * 認証を処理する * * @param \Illuminate\Http\Request $request * * @return Response */ public function authenticate(Request $request) { $credentials = $request->only('email', 'password'); if (Auth::attempt($credentials)) { // 認証に成功した return redirect()->intended('dashboard'); } } }
ここで重要なのはAuth::attempt($credentials)
です。
これが認証するか否かを判定できる仕組みで、引数に渡すのはモデルのカラム名となります。
オレオレの実装例
public function authenticate(Request $request) { // login_id カラムは email + '@' + community_id(int) で構成されたユニークの文字列として登録時に保存された値、これでログイン認証を行う $login_id = $request->email . '@' . $request->community_id; $credentials = array( 'login_id' => $login_id, 'password' => $request->password, ); $request->validate([ 'email' => 'required|string|email|max:170', 'password' => 'required|string|min:6', ]); if (Auth::attempt($credentials)) { return redirect('/')->with('message', 'ログインしました'); } else { return redirect()->back()->withErrors(array('email' => 'E-mailかPasswordが正しくありません'))->withInput(); } }
Auth::attempt($credentials)
に渡す認証の値は追加が可能です。以下の様に認証時の条件を3つ以上に設定することができます。
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) { // ユーザーは存在しており、かつアクティブで、資格停止されていない }
以降の認証に関する記述はリファレンスサイトを参考にしてください。
これ以降はケースバイケースで使用するかも。といったものが多い印象です。
よくありそうなカスタマイズについての実例など
RegisterController のカスタマイズ
ユーザー登録を行う際のvalidator
とcreate
メソッドの変更が必要であれば変える。
この辺は大変解りやすいコードだし、見たまんまで弄ってしまって基本OKです。
バリデートの変更や、ユーザー登録時に発行すべきカラムのデータ等を生成します。ありがちなのはユーザーの権限を初期状態で追加する。等の処理を行う事になるかと思います。
app\Http\Controllers\Auth\RegisterController.php
抜粋
/** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return \App\User */ protected function create(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); }
認証カスタマイズ(オレオレ編)
実はAuth::user()
で呼べるのは通常,user Tableのカラムだけとなります。ところがアプリの仕様上 user Tableがユニークにならない様な場合は、Auth::user() で欲しい一意のユーザーのデータが取得できず、偉い苦労しました。
どんなことをやったかというと、この記事にあるような事をしました。
Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法
Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得できるようにしました。
という訳で後半はこれについて説明します。