この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。
第5章では、フォルダの新規作成機能を実装します。
ルーティング
まずはルーティングを設定します。フォルダ作成機能の URL は以下のように設計しました。
URL | メソッド | 処理 |
---|---|---|
/folders/create | GET | フォルダ作成ページを表示する。 |
/folders/create | POST | フォルダ作成処理を実行する。 |
URL は同じでもメソッドの違いで機能を分けています。上記の設計を実現するために routes/web.php
に以下の2行を追加してください。
Route::get('/folders/create', 'FolderController@showCreateForm')->name('folders.create');
Route::post('/folders/create', 'FolderController@create');
ルーティングを司る Route
クラスはもうおなじみでしょうか。前章まででは get
メソッドのみを使っていましたが、post
メソッドも登場しています。Route
クラスには HTTP メソッドに応じたクラスメソッドが用意されています。
name
メソッドによるルートの命名は get
だけに定義しています。名前をつけてあとで呼び出せるのは URL だけなので、同じ URL で HTTP メソッド違いのルートがいくつかある場合はどれか一つに名前をつければ OK です。
フォームを表示する
フォームの表示(GET のルート)と作成処理(POST のルート)の順に説明していきます。
コントローラー
フォルダについての処理を受け持つ FolderController
を作成します。
$ php artisan make:controller FolderController
何を基準にコントローラークラスをまとめるか(逆にいうと分けるか)には、こうしないと動かないというような決まりはありません。いろいろな流派があると思いますが、ここでは処理の主体ごとにコントローラーを作成します。フォルダの作成ならフォルダコントローラー、タスクの編集ならタスクコントローラーといった具合です。前章、前々章で扱ったタスク一覧画面はフォルダの一覧も含まれていましたが、フォルダの一覧部分はあくまでナビゲーションと捉えてタスクコントローラーに入れました。
上記の分け方の良い点は、URL 設計やテーブル定義とある程度一貫していて予想をつけやすいところでしょうか。他の人がアプリケーションを保守するときなどに、/folder/create
という URL をみて、ここの処理が書かれているのは folder コントローラーだな、ひょっとすると folders テーブルがあるのかも、と予想をつけることができます。
さて、では app/Http/Controllers/FolderController.php
の内容を記述しましょう。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class FolderController extends Controller
{
public function showCreateForm()
{
return view('folders/create');
}
}
このルートはフォーム画面を返すだけなのでシンプルです。
テンプレート
フォーム画面のテンプレートを作成します。resources/views
に新たに folders
ディレクトリと create.blade.php
を作成してください。
$ mkdir ./resources/views/folders
$ touch ./resources/views/folders/create.blade.php
create.blade.php
の内容は以下の通りです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ToDo App</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<header>
<nav class="my-navbar">
<a class="my-navbar-brand" href="/">ToDo App</a>
</nav>
</header>
<main>
<div class="container">
<div class="row">
<div class="col col-md-offset-3 col-md-6">
<nav class="panel panel-default">
<div class="panel-heading">フォルダを追加する</div>
<div class="panel-body">
<form action="{{ route('folders.create') }}" method="post">
@csrf
<div class="form-group">
<label for="title">フォルダ名</label>
<input type="text" class="form-control" name="title" id="title" />
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
</main>
</body>
</html>
CSRF 対策
フォーム画面の内容は基本的な HTML のフォームですが、注目してほしいのは @csrf
です。
@csrf
は、CSRF トークンを含んだ input 要素を出力します。まず実際にブラウザのデベロッパーツールで見てみましょう。
Google Chrome でフォルダ作成ページを開いてから、Windows では F12
、Mac であれば command + option + i
キーでデベロッパーツールを開き、Elements タブで @csrf
を記述したはずの箇所を探します。
@csrf
を記述した箇所に以下のような input 要素が出力されているでしょう。
<input type="hidden" name="_token" value="BlpamKIhwFyLHmMLd2EJF9FrMImfSCdd200yi5ws">
value
の値は毎回変わりますが、このランダムな文字列が CSRF トークンです。
CSRF トークンとは、クロスサイトリクエストフォージェリ(Cross-Site Request Forgeries)という Web アプリケーションの脆弱性に対処するために用いられる文字列です。CSRF の詳しい説明はここでは割愛しますが、悪意のあるサイトからの POST リクエストを受け付けてしまうことで発生する脆弱性です。データベースの内容を書き換えるような処理は信頼できる特定のサイト(たいていの場合は自分のサイトのみ)のページからしか受け付けるべきではありません。
今回の例で言うと、他のサイトから /folders/create
に POST リクエストが送信されることを防ぐ必要があるということです。Web 開発初心者の方は「自分のサイトにしかフォーム画面はないのに、他のサイトから自分のサイトに POST リクエストを送れるの?」と思うかもしれませんが、実は他のサイトへのリクエスト送信はとても簡単に実現できます。フォームを置いて form
要素の action
属性の値を別のサイトにしてしまえばいいだけです。JavaScript を使っても HTTP リクエストは送信できますし、curl などのコマンドラインツールを使う方法もあります。
そこで、CSRF トークンを用いて自分のサイトからの POST リクエストだけを受け付けるようにします。まずは BlpamKIhwFyLHmMLd2EJF9FrMImfSCdd200yi5ws
のようなランダムで予測困難な値=CSRF トークンを発行し、セッションに保存します。続いて上で確認したように、フォーム画面の hidden(隠れた)input 要素の値として埋め込みます。そうするとリクエスト送信と同時にトークンも送信することになるので、トークンが含まれていて、かつセッションに入れた値と一致する場合のみ正規のリクエストとして受け入れることができます。逆にトークンが含まれない、またはトークンがセッションに入れた値と一致しないリクエストは不正だと判断できます。
CSRF は一般的に知られた Web アプリケーションの脆弱性なのでたいていの WAF では対策がなされているでしょう。Laravel でも CSRF トークンのチェックは最初から組み込まれています。すべての POST リクエストに対して CSRF トークンが要求されるため、@csrf
を書き忘れるとリクエスト送信時にエラーが発生します。Laravel はセキュリティにも配慮が行き届いたフレームワークなのですね。
さて説明が長くなってしまいましたが、フォーム画面を表示させるルートの実装は以上です。
フォルダを保存する
続いてフォルダを作成するルートを実装しましょう。
コントローラー
FolderController
に create
メソッドを追加します。
public function create(Request $request)
{
// フォルダモデルのインスタンスを作成する
$folder = new Folder();
// タイトルに入力値を代入する
$folder->title = $request->title;
// インスタンスの状態をデータベースに書き込む
$folder->save();
return redirect()->route('tasks.index', [
'id' => $folder->id,
]);
}
FolderController
で Folder
クラスを初めて使うので、ファイルの冒頭に以下の記述が必要です。
namespace App\Http\Controllers;
use App\Folder; // ★ この行を追記!
use Illuminate\Http\Request;
ポイントがいくつかありますので、一つずつ見ていきます。
入力値の取得
一つ目のポイントはユーザーの入力値をコントローラーで受け取る方法です。
コントローラーメソッドの引数に Request
クラスのインスタンスを受け入れる記述をします。
// クラスのインポート
use Illuminate\Http\Request;
class FolderController extends Controller
{
// 引数にインポートしたRequestクラスを受け入れる
public function create(Request $request)
これによって、コントローラーメソッドが呼び出されるときに Laravel がリクエストの情報を Request
クラスのインスタンス $request
に詰めて引数として渡してくれます。Request
クラスのインスタンスにはリクエストヘッダや送信元IPなどいろいろな情報が含まれていますが、その中にフォームの入力値も入っています。
$request->title;
リクエスト中の入力値は上記のようにプロパティとして取得することができます。
モデルの永続化
次のポイントはモデルクラスを永続化、つまりデータベースに書き込む処理です。
// フォルダモデルのインスタンスを作成する
$folder = new Folder();
// タイトルに入力値を代入する
$folder->title = $request->title;
// インスタンスの状態をデータベースに書き込む
$folder->save();
データベースへの書き込みは以下の手順で実装します。
- モデルクラスのインスタンスを作成する。
- インスタンスのプロパティに値を代入する。
save
メソッドを呼び出す。
これにより、モデルクラスが表すテーブルに対して INSERT が実行されます。感覚的に理解できるかもしれませんが、モデルクラスのプロパティに代入した値が各カラムに書き込まれます。
リダイレクト
最後のポイントはリダイレクトです。フォルダを作成するルートは、独自の画面を出力する必要はありません。作成できたらそのフォルダに対応するタスク一覧画面(前章・前々章で作りましたね)に遷移するのが自然でしょう。
リダイレクト処理は以下のように実装します。
return redirect()->route('tasks.index', [
'id' => $folder->id,
]);
画面を作る必要はないので view
メソッドは呼びません。代わりに redirect
メソッドを呼び出します。リダイレクト先を指定するために、redirect
メソッドに続いて route
メソッドを呼び出しています。route
メソッドの使い方はテンプレートで使ったときと同じです。
テンプレート
resources/views/tasks/index.blade.php
テンプレートに編集箇所があります。
<a href="{{ route('folders.create') }}" class="btn btn-default btn-block">
フォルダを追加する
</a>
フォルダを追加するリンクの href
を埋めてあげましょう。
ここまでで一度ブラウザで確認してみましょう。タイトル入力欄に値を入れて送信ボタンをクリックします。いかがでしょうか?タスク一覧に遷移して、フォルダ一覧には入力したタイトルが追加されていますか?次にデータベースクライアントでデータの中身を確認しましょう。folders テーブルには行が追加されているでしょうか?
続いて、よりアプリケーションを堅牢にするために入力値バリデーションを実装します。
入力値バリデーション
なぜバリデーションが必要か?
フォルダ作成ページで、タイトルを入力せずに送信ボタンをクリックしてみましょう。
上記のエラー画面が表示されたはずです。何が起こったのでしょうか?
画面の左上に書かれているエラーメッセージを確認します。
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "title" violates not-null constraint
「SQLSTATE」ということから、どうやらデータベースに関係するエラーのようです。本文は「エラー:"タイトル"カラムへの NULL 値挿入は NOT NULL 制約に違反している」と言っています。
Laravel のマイグレーションでカラムを作成するときは、指定をしない限りデフォルトで NOT NULL 制約、つまり NULL 値を入れられない制約が設定されます。タイトルを入力しないと $request->title
は NULL として扱われ、結果として folders テーブルの title カラムに NULL を入れて保存しようとしてエラーが発生します。
この挙動自体は正しいと言えます。なぜならフォルダのタイトルには何かが入力されているべきだからです。ただ、ユーザーにエラー画面を見せてしまうのはいかがなものでしょうか?エラー画面を表示する代わりに「この入力欄は必須ですよ」と教えてあげたほうが優しいですね。皆さんもそのような表示を見たことがあるはずです。そのためにはデータベースに保存する処理の前に値をチェックする必要があります。
入力値をチェックするもう一つの目的は、データの整合性を保つことです。今回はたまたまデータベース上 NOT NULL 制約がかけられていたので不正なデータは保存できないようになっていましたが、データベースの制約ではフォローしきれない仕様上のルール(例えば状態カラムは1, 2, 3のどれかであるなど)もありますので、「変な」データが入ることを防ぐためにも入力値のチェックが必要です。
データベースに書き込む前にユーザーの入力値をチェックすることを入力値バリデーションと呼びます。バリデーションとは日本語で言うと検証することですね。ここからは入力値バリデーションを実装します。
FormRequest クラス
Laravel では FormRequest クラスがバリデーションを司ります。まずは artisan コマンドでクラスを作成します。
$ php artisan make:request CreateFolder
app/Http/Requests
フォルダに CreateFolder.php
が作成されたでしょう。内容は以下の通り記述してください。雛形からの変更点に★印をつけています。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateFolder 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', // ★
];
}
}
まずは authorize
メソッドには true
を返却させます。authorize
メソッドはリクエストの内容に基づいた権限チェックのために使います。今回はこの機能は使用しないので true
を返す(つまりリクエストを受け付ける)記述のみで OK です。
さて重要なのが rule
メソッドです。ここで、入力欄ごとにチェックするルールを定義します。rule
メソッドが返却する配列がルールを表しています。
[
'title' => 'required',
]
配列のキーが入力欄です。HTML 側での input 要素の name 属性に対応します。キーに対する値の部分でルールを指定します。必須入力を意味する required
を指定しています。
この required
は Laravel がデフォルトで提供しているたくさんのルールのうちの一つです。ほかにどのようなルールがあるかはマニュアル( 公式 / 日本語)を参照してください。一般的な Web アプリケーションで必要になりそうなルールは一通り用意されています。
コントローラー
バリデーションの機能を有効にするため、コントローラー側にも編集が必要です。
以下の通り編集しましょう。ここでも変更点に★印をつけています。
use App\Http\Requests\CreateFolder; // ★ 追加
class FolderController extends Controller
{
// 中略
public function create(CreateFolder $request) // ★ 引数の型を変更
{
// 中略
}
}
まず CreateFolder
クラスをインポートして、create
メソッドの引数の型名を CreateFolder
に変更します。FormRequest クラスは先ほどまで指定していた Request クラスと互換性があります。そのためここに独自の FormRequest クラスを指定することで、入力値の取得などの Request クラスの機能はそのままに、バリデーションチェックを追加することができます。
またここで分かる通り、FormRequest クラスは基本的に一つのリクエストに対して一つ作成することになります。
エラーメッセージを表示する
最後のステップとして、テンプレートにエラー文言を表示させる記述を追加します。
<div class="panel-body">
@if($errors->any())
<div class="alert alert-danger">
<ul>
@foreach($errors->all() as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('folders.create') }}" method="post">
<!-- 中略 -->
</form>
</div>
form 要素の上に一連の記述を追加します。
バリデーションチェックの結果、ルール違反があった場合は自動的に入力画面にリダイレクトするのですが、このときルール違反の内容は $errors
変数に詰めてテンプレートに渡されます。
そこで @if($errors->any())
でルール違反があったか確認し、ある場合は @foreach($errors->all() as $message)
でエラーメッセージを列挙しています。
ではブラウザで確認しましょう。フォルダ作成ページでタイトルに何も入力せずに送信ボタンをクリックしましょう。
上記のエラーメッセージが表示されたはずです。これでユーザーにシステムエラー画面を表示させることなしに正しい入力方法を伝えることができました。
ただし、お気づきかと思いますがメッセージが英語ですね。次はこのメッセージを日本語化する方法を紹介します。
エラーメッセージを日本語化する
メッセージは resources/lang
ディレクトリで管理されています。
上の図のように、最初は en
ディレクトリに英語のメッセージ定義だけが入っています。日本語のメッセージ定義を追加するために、jp
ディレクトリを作成します。
$ mkdir ./resources/lang/jp
英語版をベースに編集するので、英語のメッセージ定義を jp
ディレクトリにコピーします。コピーするのはバリデーションメッセージを定義している validation.php
だけで OK です。
$ cp ./resources/lang/en/validation.php ./resources/lang/jp/
validation.php
を見ると、バリデーションルールに応じたメッセージがたくさん定義されているでしょう。この中で使用するものだけを日本語化すればよいです。
'required' => ':attribute は必須入力です。',
:attribute
の部分が入力欄の名前に置きかわります。
続いて日本語版の設定を参照するように config/app.php
を編集します。
'locale' => 'jp', // ★
'fallback_locale' => 'en',
locale
の設定を jp
に変更してください。デフォルトの言語設定が日本語になります。これにより、Laravel はメッセージ定義が必要になったときにまず jp
ディレクトリを探しに行きます。定義ファイルが見つかればそれを使い、見つからなければ fallback_locale
である en
のディレクトリにあるファイルを使います。
ここまででブラウザから確認してください。エラーメッセージは...
惜しい!「title」が英語のままです。入力欄の名称も日本語化しましょう。
入力欄の名称をカスタマイズするには、FormRequest クラスに attributes
メソッドを追加します。CreateFolder.php
に以下の attributes
メソッドを追記してください。
public function attributes()
{
return [
'title' => 'フォルダ名',
];
}
attributes
メソッドが返却する配列が入力欄の名称を定義します。
では再度ブラウザから確認しましょう。
うまくメッセージが日本語化されました
文字数制限を追加する
title カラムの型定義を覚えているでしょうか。VARCHAR(20)
です。つまり20文字しか入れられません。そこで必須ルールに加えて上限文字数ルールも追加します。
public function rules()
{
return [
'title' => 'required|max:20',
];
}
max:20
が入力上限20文字を意味します。複数のルールは |
で区切ります。
日本語のメッセージも用意しましょう。
'max' => [
// 中略
'string' => ':attribute は :max 文字以内で入力してください。',
// 中略
],
メッセージの :max
の部分は上限値に置きかわります。
ここまでできたらブラウザで確認してください。入力欄に21文字の値を入れて送信ボタンをクリックしましょう。「フォルダ名 は 20 文字以内で入力してください。」というメッセージが表示されたでしょうか。
入力値を復元する
メッセージは表示できましたが、最後にもう一つ解決すべき課題があります。それは入力エラーでフォーム画面に戻ってきたときに入力欄の値が消えていることです。
まぁフォルダ作成のフォーム画面にはタイトル欄しかないので入力エラーで消えていてもそこまで違和感はないかもしれませんが、ほかにも入力欄があったときに一項目の入力エラーですべての入力欄の値が消えてしまってはユーザーにとってストレスですよね。
そこで入力エラーでフォーム画面に戻ってきたときに入力欄の値を復元させます。テンプレート folders/create.blade.php
のタイトル入力欄の input 要素に value
属性を追加します。
<input type="text" class="form-control" name="title" id="title" value="{{ old('title') }}" />
value
属性の値には old('title')
の実行結果を展開しています。入力エラーがあったとき、入力値はセッションに一時的に保存されます。Laravel が提供する old
関数はそのセッション値を取得します。引数は取得したい入力欄の name
属性です。
第5章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter05 ブランチ)を参照してください。
次の章ではタスクを作成する機能を実装します。
連載記事
- 入門Laravelチュートリアル (1) イントロダクション
- 入門Laravelチュートリアル (2) ToDoアプリケーションの設計
- 入門Laravelチュートリアル (3) ToDoアプリのフォルダ一覧表示機能を作る
- 入門Laravelチュートリアル (4) ToDoアプリのタスク一覧表示機能を作る
- 入門Laravelチュートリアル (5) ToDoアプリのフォルダ作成機能を作る
- 入門Laravelチュートリアル (6) ToDoアプリのタスク作成機能を作る
- 入門Laravelチュートリアル (7) ToDoアプリのタスク編集機能を作る
- 入門Laravelチュートリアル (8) ToDoアプリの認証機能を作る
- 入門Laravelチュートリアル (9) ToDoアプリの認証機能を作る パート2
- 入門Laravelチュートリアル (10) エラーハンドリング
- 入門Laravelチュートリアル (11) ToDoアプリをHerokuにデプロイする