前回の第2章で Vue コンポーネントを分割し、ソースコードの見通しがよくなりました。
しかしまだ大事な機能を実装していません。
今回作った固定ページ管理は「タイトル」「スラッグ(URL に使う ID みたいなやつ)」「本文」の3つのパラメータからできていますが、すべて入力必須、さらにスラッグは一意でないといけません。
Laravel の API にバリデーションの機能を組み込むとともに、モーダルの編集フォームにエラー表示を追加していきましょう。
FormRequest の拡張
「Vue.js と本気で戦ってみた」という記事タイトルのくせして、ここから Laravel 側の作業。
まずバリデーションを行なう FormRequest
という機能を拡張します。
>php artisan make:request AdminPageApiRequest
app/Http/Requests/AdminPageApiRequest.php
というファイルが生まれました。
ここに固定ページのバリデーションを書いていきます。
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Validator; use Illuminate\Http\Exceptions\HttpResponseException; use App\Page; class AdminPageApiRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required|max:100', 'slug' => 'required|max:20', 'content' => 'required', ]; } public function withValidator(Validator $validator) { $validator->after(function ($validator) { $page = Page::where('slug', $this->input('slug'))->first(); if ( $page && $this->input('id') !== $page->id ) { $validator->errors()->add('slug', 'slug must be unique.'); } }); } protected function failedValidation(Validator $validator) { $response = response()->json([ 'status' => 400, 'errors' => $validator->errors(), ], 400); throw new HttpResponseException($response); } } // class AdminPageApiRequest
authorize()
は認証が必要な場合に使うようです。今回の API はルーティングの時点で認証まわりを済ませているので、無条件で通すように設定。
rules()
で具体的なバリデーションルールを書いていきます。DB 定義に従って、title
は必須+100文字以内、slug
は必須+20文字以内、content
は必須指定のみとしました。
また、withValidator()
メソッドで slug
のユニークチェックをします。パラメータとして渡ってきた id
と slug
で、すでに登録されているデータがないことをチェック。すでに存在する場合はエラーメッセージを出します。
そして failedValidation()
メソッドで、エラーがあったときの JSON レスポンスを定義します。ここでは 400 Bad Request
を返すようにしました。
コントローラへの組み込み
こうしてできたバリデーションを API コントローラに組み込みましょう。
<?php namespace App\Http\Controllers\Api\Admin; use App\Http\Controllers\Controller; use App\Http\Requests\AdminPageApiRequest; use Illuminate\Http\Exceptions\HttpResponseException; use App\Page; class PageController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $pages = Page::all(); $result = []; foreach ($pages as $page) { $content = strip_tags($page->content); if ( mb_strlen($content) > 200 ) { $page->content = mb_substr($content, 0, 200) . '…'; } $result[$page->id] = $page; } return response($result); } /** * Store a newly created resource in storage. * * @param \App\Http\Requests\AdminPageApiRequest $request * @return \Illuminate\Http\Response */ public function store(AdminPageApiRequest $request) { // } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { $page = Page::find($id); if ( ! $page ) { $response = response()->json([ 'status' => 'error', 'message' => 'Page not found', ], 404); throw new HttpResponseException($response); } return response($page); } /** * Update the specified resource in storage. * * @param \App\Http\Requests\AdminPageApiRequest $request * @param int $id * @return \Illuminate\Http\Response */ public function update(AdminPageApiRequest $request, $id) { $page = Page::find($id); if ( ! $page ) { $response = response()->json([ 'status' => 'error', 'message' => 'Page not found', ], 404); throw new HttpResponseException($response); } $page->title = $request->title; $page->slug = $request->slug; $page->content = $request->content; $page->save(); $result = array( 'status' => 'success', 'message' => '', ); return response($result); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { // } }
引数のタイプヒンティングが Request
だったのを AdminPageApiRequest
に差し替えるだけ。これだけでバリデーションが動くようになります。
store()
メソッドはまだ未実装ですが、ページの新規作成を作るときに実装する予定なので、今のうちにバリデーションを組み込んでおきます。
表示調整
さて、Vue に戻りましょう。
入力フォームでエラーを受け取りたいので、edit-page-modal
コンポーネントにエラー処理を書いていきます。
<template> <div id="edit-page" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="edit-page-label" aria-hidden="true"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <input type="hidden" name="id" id="page-id" v-bind:value="id" /> <h5 class="modal-title" id="edit-page-label">Edit page</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <form v-on:submit.prevent="save(id)"> <div class="modal-body"> <div class="form-group" v-bind:class="{ 'is-invalid': errors.slug }"> <label for="page-title">Title</label> <input type="text" class="form-control" id="page-title" v-model="title" v-bind:class="{ 'is-invalid': errors.title }" /> <label class="invalid-feedback" role="alert" for="page-title" v-if="errors.title" v-html="errors.title"></label> </div> <div class="form-group" v-bind:class="{ 'is-invalid': errors.slug }"> <label for="page-slug">Slug</label> <input type="text" class="form-control" id="page-slug" v-model="slug" v-bind:class="{ 'is-invalid': errors.slug }" /> <label class="invalid-feedback" role="alert" for="page-slug" v-if="errors.slug" v-html="errors.slug"></label> </div> <div class="form-group" v-bind:class="{ 'is-invalid': errors.slug }"> <label for="page-content">Content</label> <textarea class="form-control" id="page-content" v-model="content" v-bind:class="{ 'is-invalid': errors.content }" rows="10" ></textarea> <label class="invalid-feedback" role="alert" for="page-content" v-if="errors.content" v-html="errors.content"></label> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal" >Cancel</button> <button type="submit" class="btn btn-primary" >Save</button> </div> </form> </div> </div> </div> </template> <script> export default { name: "edit-page-modal", data: function () { return { urlShow: "", urlUpdate: "", id: 0, title: "", slug: "", content: "", errors: { title: "", slug: "", content: "", } } }, methods: { init: function (id, urlShow, urlUpdate) { console.log("modal::init"); // initialize this.urlShow = urlShow; this.urlUpdate = urlUpdate; this.id = 0; this.title = ""; this.slug = ""; this.content = ""; this.errors.title = ""; this.errors.slug = ""; this.errors.content = ""; // get detail var url = this.urlShow; console.log("get", url); axios.get(url) .then(response => { this.id = response.data.id; this.title = response.data.title; this.slug = response.data.slug; this.content = response.data.content; }) .catch(response => { alert(response); }); // show modal var el = "#" + this.$el.id; $(el).modal(); }, save: function () { console.log("modal::save", this.id); // update var url = this.urlUpdate; console.log("put", url); axios.put(url, { id: this.id, title: this.title, slug: this.slug, content: this.content }) .then(response => { // hide modal var el = "#" + this.$el.id; $(el).modal('hide'); // redraw list this.$emit("redraw"); }) .catch(response => { var status = response.response.status; if (status == 400) { var errors = response.response.data.errors; this.errors.title = ( errors.title ? errors.title[0] : "" ); this.errors.slug = ( errors.slug ? errors.slug[0] : "" ); this.errors.content = ( errors.content ? errors.content[0] : "" ); } }); return false; }, redraw: function (id) { this.$emit("init"); } } } </script>
まず data
にエラーメッセージを格納するプロパティを追加<し、init()
メソッドで初期化します。
save()
メソッドで 400 エラーが返ってきたらプロパティにエラーメッセージを格納。
このメッセージがトリガーとなって、<label class="invalid-feedback">
にエラーメッセージが表示されます。
ついでにエラーが見やすいよう、入力項目に is-invalid
のクラスを指定します。
これで敢えてエラーを含むデータを送信すると……
Bootstrap を利用したエラー表示が完成です!
おわりに
Laravel を勉強し始めてから3ヶ月経ち、だいぶ慣れてきた感じがします。
FuelPHP のころはバリデーションはモデルに書いてたので、文化の違いというかカルチャーショックに似たものを感じました。Request
オブジェクトの差し替えだけで動くんかい!!
肝心の Cities History Project は、現状と同等のものが完成したら Laravel バージョンに差し替える予定です。
それではまた!