作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 4章 後半資料 レスポンス ミドルウェア

以下の記事は2019年8月8日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 HTTPリクエストとレスポンスの輪読会資料の一部となります。
今回は 4章後半 4-3『レスポンス』, 4-4『ミドルウェア』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

また書籍のコードが記載されている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

hal+json.png

リソースクラスの基本

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;
    }
}

ヘルパ関数 info

レスポンスヘッダのログ出力

上のコードにある後半部分がレスポンスヘッダをログ出力する部分、以下にも記述 レスポンスヘッダを取得するには、ミドルウェアクラス内の $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">