2018.12.10

入門Laravelチュートリアル (5) ToDoアプリのフォルダ作成機能を作る


この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。

👇 電子書籍版も公開しています。

第5章では、フォルダの新規作成機能を実装します。

フォルダ作成ページ

ルーティング

まずはルーティングを設定します。フォルダ作成機能の URL は以下のように設計しました。

URL メソッド 処理
/folders/create GET フォルダ作成ページを表示する。
/folders/create POST フォルダ作成処理を実行する。

URL は同じでもメソッドの違いで機能を分けています。上記の設計を実現するために routes/web.php に以下の2行を追加してください。

web.php
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 の内容を記述しましょう。

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 の内容は以下の通りです。

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 を記述したはずの箇所を探します。

Developer Tool

@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 はセキュリティにも配慮が行き届いたフレームワークなのですね。

さて説明が長くなってしまいましたが、フォーム画面を表示させるルートの実装は以上です。

フォルダを保存する

続いてフォルダを作成するルートを実装しましょう。

コントローラー

FolderControllercreate メソッドを追加します。

FolderController.php
public function create(Request $request)
{
    // フォルダモデルのインスタンスを作成する
    $folder = new Folder();
    // タイトルに入力値を代入する
    $folder->title = $request->title;
    // インスタンスの状態をデータベースに書き込む
    $folder->save();

    return redirect()->route('tasks.index', [
        'id' => $folder->id,
    ]);
}

FolderControllerFolder クラスを初めて使うので、ファイルの冒頭に以下の記述が必要です。

FolderController.php
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();

データベースへの書き込みは以下の手順で実装します。

  1. モデルクラスのインスタンスを作成する。
  2. インスタンスのプロパティに値を代入する。
  3. save メソッドを呼び出す。

これにより、モデルクラスが表すテーブルに対して INSERT が実行されます。感覚的に理解できるかもしれませんが、モデルクラスのプロパティに代入した値が各カラムに書き込まれます。

リダイレクト

最後のポイントはリダイレクトです。フォルダを作成するルートは、独自の画面を出力する必要はありません。作成できたらそのフォルダに対応するタスク一覧画面(前章・前々章で作りましたね)に遷移するのが自然でしょう。

リダイレクト処理は以下のように実装します。

return redirect()->route('tasks.index', [
    'id' => $folder->id,
]);

画面を作る必要はないので view メソッドは呼びません。代わりに redirect メソッドを呼び出します。リダイレクト先を指定するために、redirect メソッドに続いて route メソッドを呼び出しています。route メソッドの使い方はテンプレートで使ったときと同じです。

テンプレート

resources/views/tasks/index.blade.php テンプレートに編集箇所があります。

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 が作成されたでしょう。内容は以下の通り記述してください。雛形からの変更点に★印をつけています。

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 アプリケーションで必要になりそうなルールは一通り用意されています。

コントローラー

バリデーションの機能を有効にするため、コントローラー側にも編集が必要です。
以下の通り編集しましょう。ここでも変更点に★印をつけています。

FolderController.php
use App\Http\Requests\CreateFolder; // ★ 追加

class FolderController extends Controller
{
    // 中略

    public function create(CreateFolder $request) // ★ 引数の型を変更
    {
        // 中略
    }
}

まず CreateFolder クラスをインポートして、create メソッドの引数の型名を CreateFolder に変更します。FormRequest クラスは先ほどまで指定していた Request クラスと互換性があります。そのためここに独自の FormRequest クラスを指定することで、入力値の取得などの Request クラスの機能はそのままに、バリデーションチェックを追加することができます。

またここで分かる通り、FormRequest クラスは基本的に一つのリクエストに対して一つ作成することになります。

エラーメッセージを表示する

最後のステップとして、テンプレートにエラー文言を表示させる記述を追加します。

create.blade.php
<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 ディレクトリで管理されています。

lang ディレクトリ

上の図のように、最初は en ディレクトリに英語のメッセージ定義だけが入っています。日本語のメッセージ定義を追加するために、jp ディレクトリを作成します。

$ mkdir ./resources/lang/jp

英語版をベースに編集するので、英語のメッセージ定義を jp ディレクトリにコピーします。コピーするのはバリデーションメッセージを定義している validation.php だけで OK です。

$ cp ./resources/lang/en/validation.php ./resources/lang/jp/

validation.php を見ると、バリデーションルールに応じたメッセージがたくさん定義されているでしょう。この中で使用するものだけを日本語化すればよいです。

validation.php
'required'             => ':attribute は必須入力です。',

:attribute の部分が入力欄の名前に置きかわります。

続いて日本語版の設定を参照するように config/app.php を編集します。

app.php
'locale' => 'jp', // ★
'fallback_locale' => 'en',

locale の設定を jp に変更してください。デフォルトの言語設定が日本語になります。これにより、Laravel はメッセージ定義が必要になったときにまず jp ディレクトリを探しに行きます。定義ファイルが見つかればそれを使い、見つからなければ fallback_locale である en のディレクトリにあるファイルを使います。

ここまででブラウザから確認してください。エラーメッセージは...

エラーメッセージ

惜しい!「title」が英語のままです。入力欄の名称も日本語化しましょう。

入力欄の名称をカスタマイズするには、FormRequest クラスに attributes メソッドを追加します。CreateFolder.php に以下の attributes メソッドを追記してください。

CreateFolder.php
public function attributes()
{
    return [
        'title' => 'フォルダ名',
    ];
}

attributes メソッドが返却する配列が入力欄の名称を定義します。

では再度ブラウザから確認しましょう。

エラーメッセージ

うまくメッセージが日本語化されました 🎉

文字数制限を追加する

title カラムの型定義を覚えているでしょうか。VARCHAR(20) です。つまり20文字しか入れられません。そこで必須ルールに加えて上限文字数ルールも追加します。

CreateFolder.php
public function rules()
{
    return [
        'title' => 'required|max:20',
    ];
}

max:20 が入力上限20文字を意味します。複数のルールは | で区切ります。

日本語のメッセージも用意しましょう。

validation.php
'max'                  => [
    // 中略
    'string'  => ':attribute は :max 文字以内で入力してください。',
    // 中略
],

メッセージの :max の部分は上限値に置きかわります。

ここまでできたらブラウザで確認してください。入力欄に21文字の値を入れて送信ボタンをクリックしましょう。「フォルダ名 は 20 文字以内で入力してください。」というメッセージが表示されたでしょうか。

入力値を復元する

メッセージは表示できましたが、最後にもう一つ解決すべき課題があります。それは入力エラーでフォーム画面に戻ってきたときに入力欄の値が消えていることです。

まぁフォルダ作成のフォーム画面にはタイトル欄しかないので入力エラーで消えていてもそこまで違和感はないかもしれませんが、ほかにも入力欄があったときに一項目の入力エラーですべての入力欄の値が消えてしまってはユーザーにとってストレスですよね。

そこで入力エラーでフォーム画面に戻ってきたときに入力欄の値を復元させます。テンプレート folders/create.blade.php のタイトル入力欄の input 要素に value 属性を追加します。

create.blade.php
<input type="text" class="form-control" name="title" id="title" value="{{ old('title') }}" />

value 属性の値には old('title') の実行結果を展開しています。入力エラーがあったとき、入力値はセッションに一時的に保存されます。Laravel が提供する old 関数はそのセッション値を取得します。引数は取得したい入力欄の name 属性です。

🦄 🦄 🦄

第5章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter05 ブランチ)を参照してください。

次の章ではタスクを作成する機能を実装します。

連載記事