Open4

PHP/Laravel アプリ開発時に知っていると便利かもしれないTips

KenKen

Laravelアプリ開発時に知っていると便利かもしれないTipsをこちらに投稿していきます。
初投稿?初スクラップ?のため不慣れな点が多々あります。ご容赦の程よろしくお願いいたします。
また、PHPは門外漢のため、慣例を無視したものや不自然な記述等あるかもしれません。
その点もご留意ください。

Laravelのバージョンは6.2にて確認を行っています。
バージョンの差異によって動作に差異がある場合があります。

指摘や情報展開等、ご自由にコメントいただけると幸いです。

KenKen

Laravelのcsrfトークンを用いて二重サブミットを抑止する

Laravelのsessionオブジェクトは StartSession.php にて以下の仕様となっている。

  1. リクエストを受け取った直後、sessionストレージから情報を取得し、
    Store.php のインスタンスとして状態を保持する。
  2. レスポンスの直前に Store.php インスタンスの状態をsessionストレージに保存する。

sessionの状態が保存される前に受け取った複数のリクエスト間にて、session情報は最新の状態で同期されない。
(リクエストを受け取り、上記1. の処理後、2. の処理前の間に別のリクエストを受け取ると、同じsession idのリクエストであっても、sessionオブジェクトの情報に差異が発生する可能性がある)

そのため、submitボタンの連打等によって多重送信を受け取った際、最初のリクエストにてsessionオブジェクトのcsrfトークンの値を変更したとしても、2つ目以降のリクエストが取得したsessionオブジェクトには反映されておらず、そのままの状態では多重送信の抑止が行えない。

csrfトークンを用いて多重サブミットを抑止したい場合、抑止したい処理にて明示的に最新のsession情報を取得し、クライアントのパラメータの値と比較した後、問題がなければ新たなトークンを生成してsessionを保存するといった処理を行う必要がある。

Trait等に以下のような処理を定義する。

Trait.php
private function multiSubmitCheck(Request $request)
{
    // Sessionオブジェクト(Store.php)
    $session = $request->session();
    // Sessionオブジェクトを最新化
    $session->start();
    // csrfトークンと画面パラメータのcsrfトークンの値が異なる場合エラー
    if ($session->token() != $request->input('_token')) {
        return false;
    }
    // csrfトークンの再生成 
    // Store #regenerate によるセッションID再生成でもトークンの再生成が行われる
    $session->regenerateToken();
    // Sessionを保存
    $session->save();

    return true;
}

そして多重サブミットを防ぎたい処理にて上記処理を呼び出し、
戻り値にfalseを返すようであればabortヘルパでエラーを返すなりしてやればよい。

Controller.php
public function update(Request $request)
{
    // 多重サブミットチェック
    if (!this->multiSubmitCheck($request)) abort(409);
    ...
}

加えて、一般的なクライアントサイドにおける多重submit抑止の処理も、不要リクエストの抑制、意図せぬエラー発生の防止等期待できるため、可能な限り行うべきだと思われる。

app.js
const triggers = $('a,button')
$('form').on('submit', function () {
    triggers.css('pointer-events', 'none')
})
KenKen

デフォルトのページネーション機能をpostによるsubmitに変更

bladeにてページネーション結果の表示を行う際、各ページのボタンはリンクとして生成され、ページボタンを押下するとgetによる画面遷移が行われる。様々な理由により、検索処理はpostによるリクエストに紐づけて作成しており、ページネーション機能もpostによるsubmitの方が都合がよいといった場面があると思われる。
Eloquent queryのpaginateメソッドは、requestのパラメータ内にpageというキーが含まれてさえいればgetだろうとpostだろうと問題なく動作するようなので、ページネーションボタン押下時の処理にカスタマイズを入れる。

まず、JavaScriptにてページボタン押下時の処理を定義する。

app.js
function pagination(page) {
    const form = $(event.target).closest('form')
    const action = `${form.attr('action')}?page=${page}`
    form.attr('action', action).submit()
}

後はページボタン押下時に上記処理を呼び出すだけ。
こちらを参考にページネーションビューを出力し、
bootstrap-4.blade.phpファイルに対して以下のカスタマイズを行う。

bootstrap-4.blade.php
{{-- 11行目を以下に書き換え --}}
<a class="page-link" rel="prev" aria-lavel="@lang('pagination.previous')" onclick="pagination({{ $paginator->currentPage() - 1 }});">&lsaquo;</a>

{{-- 29行目を以下に書き換え --}}
<a class="page-link" onclick="pagination({{ $page }});">{{ $page }}</a>

{{-- 39行目を以下に書き換え --}}
<a class="page-link" rel="next" aria-lavel="@lang('pagination.next')" onclick="pagination({{ $paginator->currentPage() + 1 }});">&rsaquo;</a>

このカスタマイズはページボタンの直近の祖先(親)要素のformに対して、クエリストリングでpageパラメータを付与した後にsubmitしているだけとなる。状況に応じて適宜内容変更することで、様々なケースに対応できると思われる。

KenKen

404発生時に認証ディレクティブが正常に動作しない問題への対策

404発生時、bladeにて@authや@guestといった認証ディレクティブが、
認証状態にかかわらず@guestの分岐となる場合がある。
レイアウトのbladeにてログイン状態に応じてログイン/ログアウトのボタンの出し分けを行っている場合等、少々不都合があるため対策を講じる必要がある。

内部実装を詳細に追ったわけではないので正確な情報は不明だが、認証ディレクティブを正常に動作させるためには、ルート定義ファイルにリクエストを経由させることが必要なようである。
そのため、未定義ルートでも一度 web.php にて拾ってやり、そのうえで404を返してやることで認証ディレクティブが正常に動作した。

web.php
// リクエストが他ルートに一致しない場合
Route::fallback(function () {
    abort(404);
});

情報求む