【輪読会資料】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">