Laravel5.7: バリデーション(posts)


投稿された記事を保存する前に入力内容を検査し、エラーがあれば前のページに戻すようにします。

親記事

Laravel 5.7で基本的なCRUDを作る - Qiita

ルールをフォームリクエストにまとめる

バリデーション・ルールはコントローラのアクション内に記述できますが、複数のアクションで使いまわせるようにフォームリクエストにまとめます。
readouble.com: フォームリクエストバリデーション

PowerShell
# Post用のフォームリクエストを生成
> php artisan make:request StorePost
app/Http/Requests/StorePost.php
    public function authorize()
    {
        // 認可は別の箇所で行うので、ここでは素通りさせる
        return true;
    }

    public function rules()
    {
        return [
            // 題名は191文字まで、本文は400文字まで
            'title' => 'required|max:191',
            'body' => 'required|max:400',
        ];
    }

コントローラのアクションでの引数の型をRequestからStorePostに変えることで、これらのアクションの前に自動的にバリデーションが実行されます。

app/Http/Controllers/PostController.php
 // インポートを忘れずに!!!!
+use App\Http\Requests\StorePost;

 class PostController extends Controller
 {
     /**
      * 新しい記事を保存する
      *
-     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Http\Requests\StorePost $request
      * @return \Illuminate\Http\Response
      */
-    public function store(Request $request)
+    public function store(StorePost $request)
     {

(中略)

     /**
      * 記事の更新を保存する
      *
-     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Http\Requests\StorePost $request
      * @param  \App\Post $post
      * @return \Illuminate\Http\Response
      */
-    public function update(Request $request, Post $post)
+    public function update(StorePost $request, Post $post)
     {

ビューを修正する

バリデーションが通らずに投稿ページへ戻された場合は、ヘルパー関数のoldで入力内容を復元し、hasメソッドでその入力欄にエラーがあったかどうかを確認できます。
なお、バリデーションが通らなかった項目を赤く強調するためにBootstrapのCSSクラスを使っています。
Bootstrap: Validation - Server side

resources/views/posts/create.blade.php
         <div class="form-group">
             <label for="title">{{ __('Title') }}</label>
-            <input id="title" type="text" class="form-control" name="title" required autofocus>
+            <input id="title" type="text" class="form-control @if ($errors->has('title')) is-invalid @endif" name="title" value="{{ old('title')+ }}" required autofocus>
+            @if ($errors->has('title'))
+                <span class="invalid-feedback" role="alert">
+                    {{ $errors->first('title') }}
+                </span>
+            @endif
         </div>
         <div class="form-group">
             <label for="body">{{ __('Body') }}</label>
-            <textarea id="body" class="form-control" name="body" rows="8" required></textarea>
+            <textarea id="body" class="form-control @if ($errors->has('body')) is-invalid @endif" name="body" rows="8" required>{{ old('body')+ }}</textarea>
+            @if ($errors->has('body'))
+                <span class="invalid-feedback" role="alert">
+                    {{ $errors->first('body') }}
+                </span>
+            @endif
         </div>

old関数の第2引数でデフォルト値を設定できます。
編集用フォームでは、この第2引数が役に立ちます。

// title欄の前回の入力値があればそれを、
// なければDBに保存されている値 $post->title を返す。
old('title', $post->title)
resources/views/posts/edit.blade.php
         <div class="form-group">
             <label for="title">{{ __('Title') }}</label>
-            <input id="title" type="text" class="form-control" name="title" value="{{ $post->title }}" required autofocus>
+            <input id="title" type="text" class="form-control @if ($errors->has('title')) is-invalid @endif" name="title" value="{{ old('title', $post->title) }}" required autofocus>
+            @if ($errors->has('title'))
+                <span class="invalid-feedback" role="alert">
+                    {{ $errors->first('title') }}
+                </span>
+            @endif
         </div>
         <div class="form-group">
             <label for="body">{{ __('Body') }}</label>
-            <textarea id="body" class="form-control" name="body" rows="8" required>{{ $post->body }}</textarea>
+            <textarea id="body" class="form-control @if ($errors->has('body')) is-invalid @endif" name="body" rows="8" required>{{ old('body'+, $post->body) }}</textarea>
+            @if ($errors->has('body'))
+                <span class="invalid-feedback" role="alert">
+                    {{ $errors->first('body') }}
+                </span>
+            @endif
         </div>

フィールド名の翻訳を指定する

バリデーションが通らなかった場合に表示する日本語メッセージは、以前の記事で既に作っています。
たとえば、題名の文字数が多すぎる場合は下記のテンプレートが適用されます。

resources/lang/ja/validation.php
    'max' => [
        'string'  => ':attribute は :max 文字以下にしてください。',

しかし、このままでは:attributeだけが翻訳されず、「title は 191 文字以下にしてください。」のように表示されてしまいます。

そこで、下記のようにカスタム属性名を末尾に記述します。
readouble.com: 言語ファイル中のカスタム属性名の指定

resources/lang/ja/validation.php
    'attributes' => [
        'title' => __('Title'),
        'body'  => __('Body'),
    ],

これでtitle題名に変換されます。

(余談) 入力内容が復元されないトラブル

バリデーションが通らなかった場合にページは戻されるものの、入力内容が復元されず空っぽになるというトラブルに見舞われました。
Laravel Debugbarで確認したところ、本来なら下の画像のようにセッションに_old_inputerrorsが保存されているはずですが、キーも含めて丸ごと存在していません。

このトラブルの原因は、セッションの保存先をクッキーにしていたせいでした。
下記のようにreadouble.com管理人の川瀬さんから助言をいただきました。
いずれはRedisを使うつもりですが、とりあえず保存先はデフォルトのファイルにします。

:link: https://twitter.com/HiroKws/status/887597436587220992

日本語(UTF−8)をクッキーに保存する場合にエンコードされます。そのエンコードされた値が更に暗号化されるため、短いメッセージのつもりでもクッキーの最大長である4KBを超えてしまいます。そのため、セッションのデフォルト保存先をファイルにしているフレームワークが多いです。

このトラブルの原因を探るために、ずいぶんと試行錯誤しました。
はじめは、素直に前のページに戻らず、二重にリダイレクトしているのではないかと疑いました。
そしてリダイレクトをする可能性のあるミドルウェアを無効にしたものの、解決せず。
ログにも何の記録もなし。

ただ、文字数が多すぎるとセッションが消えてしまうということだけは分かりました。
はじめは日本語のようなマルチバイト文字が原因かと思いましたが、ASCII文字だけでも発生します。
そこまで迫っていながらクッキーの最大容量に気づかず、ソースコード内でバリデーションを実際に行っている箇所を確認しようとしていました。