2018.12.10

入門Laravelチュートリアル (8) ToDoアプリの認証機能を作る


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

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

第8章では、認証機能を実装します。会員登録とログイン機能に加えて、ログインしたユーザーは自分のフォルダのタスクだけを閲覧できる機能も実装します。

ユーザーとフォルダを紐づける

まずデータ構造を整えるところから始めましょう。

認証機能の追加にともなって、データ構造としてはまず会員ユーザーが存在して、ユーザーごとに自分のフォルダを作っていく形にしたいと思います。

つまりタスクがフォルダに紐づいているのと同じパターンで、フォルダはユーザーに紐づけます。タスクとフォルダとの関係性をデータ構造で表現するためにどのようなテーブル設計を行ったか覚えていますか?タスクテーブルに、何番のフォルダに紐づいているか、フォルダIDのカラムを持たせたのでしたね。今回も同様に、フォルダテーブルにユーザーIDカラムを追加することでユーザーとフォルダの関係性を表現します。

マイグレーション

テーブル定義に変更を加える場合もマイグレーションの機能を利用します。

$ php artisan make:migration add_user_id_to_folders --table=folders

database/migrations/2018_11_25_155051_create_tasks_table.php が作成されますので、以下の内容で編集してください(2018_11_25_155051 の部分は実行日時によって変わります)。

Y_m_d_His_create_tasks_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddUserIdToFolders extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('folders', function (Blueprint $table) {
            $table->integer('user_id')->unsigned();

            // 外部キーを設定する
            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('folders', function (Blueprint $table) {
            $table->dropColumn('user_id');
        });
    }
}

up メソッドでは user_id カラムを追加して外部キーを設定する処理を記述しています。down メソッドでは逆に user_id カラムを削除する処理を記述しています。

マイグレーションを実行してみましょう。

$ php artisan migrate

すると、以下のようにエラーが表示されマイグレーションがうまくいかないはずです。

Migration error

エラーメッセージを確認してみます。

SQLSTATE[23502]: Not null violation: 7 ERROR: column "user_id" contains null values (SQL: alter table "folders" add column "user_id" integer not null)

user_id カラムは NOT NULL なのに NULL 値が入れられようとした、という内容です。

カラムを追加しただけでデータを入れていないのに、なぜこのエラーが出たかというと、folders テーブルにはすでにシーダーや画面から入れたデータが入っているからです。カラムを追加するということはもちろん既存のデータ行にも user_id が増えるのですが、このときデータベースは NULL を入れようとします。そこでそのカラムが NOT NULL だとエラーになるというわけです。

回避策としては、NULL 許容にするか、NULL ではないデフォルト値を設定するか、データを捨ててテーブルを作り直すか、のどれかになるでしょう。今回の user_id カラムは外部キーなので NULL を許容できませんしデフォルト値を設定するのも変です。そこでテーブルを作り直します。

$ php artisan migrate:fresh

この migrate:fresh コマンドはテーブルをすべて削除してマイグレーションを実行し直します。

Migrate refresh

すべてのマイグレーションが実行されていますね。

ユーザーモデル

次にユーザーとフォルダの関係性をモデルにも記述します。

User.php
class User extends Authenticatable
{
    // 中略

    public function folders()
    {
        return $this->hasMany('App\Folder');
    }
}

Folder モデルに hasMany メソッドを追加したのと同じパターンです。

シーダーを再作成する

続いてシーダーを作り直しましょう。すべて捨ててしまったデータを再作成します。

ユーザー

まずはユーザーテーブルのためのシーダーを追加します。

$ php artisan make:seeder UsersTableSeeder

database/seeds/UsersTableSeeder.php が作成されるので、以下の内容で編集してください。

UsersTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('users')->insert([
            'name' => 'test',
            'email' => 'dummy@email.com',
            'password' => bcrypt('test1234'),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
    }
}

ポイントは bcrypt 関数です。データベース上、ユーザーのパスワードは必ず暗号化してデータベースに保存します。そのまま(「平文」と言います)では保存しません。bcrypt 関数は与えられた文字列の暗号化を行います。

フォルダ

次にフォルダテーブル用のシーダーを編集します。追加する行に星印をつけています。

FoldersTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class FoldersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = DB::table('users')->first(); // ★

        $titles = ['プライベート', '仕事', '旅行'];

        foreach ($titles as $title) {
            DB::table('folders')->insert([
                'title' => $title,
                'user_id' => $user->id, // ★
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}

first メソッドでユーザーを一行だけ取得して、その ID を user_id の値に指定しています。

ちなみにタスクテーブルはユーザーテーブルとの直接的な関係はありませんので、シーダーを修正する必要もありません。

実行

三種類のシーダーを実行しましょう。

$ php artisan db:seed --class=UsersTableSeeder
$ php artisan db:seed --class=FoldersTableSeeder
$ php artisan db:seed --class=TasksTableSeeder

それぞれ Database seeding completed successfully. と出力されたら OK です。

ホームページを作成する

ここでホームページ(トップページ)を追加しておきます。これから会員登録やログインの機能を実装しますが、登録やログインが完了した後のリダイレクト先として使います。

ルーティング

/ へのアクセスがホーム画面を返すように記述を追加します。

web.php
Route::get('/', 'HomeController@index')->name('home');

テンプレート

resources/views/home.blade.php を以下の内容で作成してください。

home.blade.php
@extends('layout')

@section('content')
  <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">
            <div class="text-center">
              <a href="{{ route('folders.create') }}" class="btn btn-primary">
                フォルダ作成ページへ
              </a>
            </div>
          </div>
        </nav>
      </div>
    </div>
  </div>
@endsection

フォルダの作成を促す内容にしてみました。

コントローラー

続いてコントローラーを作成します。

$ php artisan make:controller HomeController

内容は、index メソッドが先ほど作成したテンプレートの画面を返すだけです。

HomeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{
    public function index()
    {
        return view('home');
    }
}

これでいったんホームページの作成は完了です。

いよいよ会員登録機能を実装していきましょう。

会員登録機能

Laravel には認証機能が最初から搭載されています。認証機能を受け持つコントローラーは app/Http/Controllers/Auth ディレクトリにすでに用意されています。ルーティングについても認証用の設定を吐き出すメソッドが用意されているので、基本的にはテンプレートを作成するだけでアプリケーションに認証機能を追加することができます。

認証機能まで面倒を見てくれるフレームワークはなかなかないです。この道具としての完成度が Laravel の魅力の一つだと思います。

ルーティング

さて、ではルーティングから始めましょう。といっても以下の一行を追加するだけです。

web.php
Auth::routes();

このメソッドが、会員登録・ログイン・ログアウト・パスワード再設定の各機能で必要なルーティング設定をすべて定義します。

ユーザーデータを確認する

テンプレートの実装に進む前に、ユーザーテーブルの構成を確認しておきましょう。

あらかじめ database/migrations にユーザーテーブルを作成するためのマイグレーションファイル 2014_10_12_000000_create_users_table.php が組み込まれています。今回はこのテーブル定義をそのまま使いました。

2014_10_12_000000_create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

整形された形でのテーブル定義は以下の通りです。

users

こちらのテーブル定義を前提に実装を進めていきます。

テンプレート

以下の内容で resources/views/auth/register.blade.php を作成してください。

register.blade.php
@extends('layout')

@section('content')
  <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">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('register') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="name">ユーザー名</label>
                <input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password">
              </div>
              <div class="form-group">
                <label for="password-confirm">パスワード(確認)</label>
                <input type="password" class="form-control" id="password-confirm" name="password_confirmation">
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-primary">送信</button>
              </div>
            </form>
          </div>
        </nav>
      </div>
    </div>
  </div>
@endsection

二つのポイントについて説明します。

認証機能のルート

まず一つ目は認証機能のルート名についてです。

<form action="{{ route('register') }}" method="POST">

上記のように route という名前を指定していますが、このルート名は Auth::routes() で定義されています。これはルーティングの定義を一覧表示する route:list で確認できます。

$ php artisan route:list

+--------+----------+-----------------------------------+------------------+------------------------------------------------------------------------+--------------+
| Domain | Method   | URI                               | Name             | Action                                                                 | Middleware   |
+--------+----------+-----------------------------------+------------------+------------------------------------------------------------------------+--------------+
|        | GET|HEAD | /                                 | home             | App\Http\Controllers\HomeController@index                              | web          |
|        | GET|HEAD | api/user                          |                  | Closure                                                                | api,auth:api |
|        | GET|HEAD | folders/create                    | folders.create   | App\Http\Controllers\FolderController@showCreateForm                   | web          |
|        | POST     | folders/create                    |                  | App\Http\Controllers\FolderController@create                           | web          |
|        | GET|HEAD | folders/{id}/tasks                | tasks.index      | App\Http\Controllers\TaskController@index                              | web          |
|        | GET|HEAD | folders/{id}/tasks/create         | tasks.create     | App\Http\Controllers\TaskController@showCreateForm                     | web          |
|        | POST     | folders/{id}/tasks/create         |                  | App\Http\Controllers\TaskController@create                             | web          |
|        | GET|HEAD | folders/{id}/tasks/{task_id}/edit | tasks.edit       | App\Http\Controllers\TaskController@showEditForm                       | web          |
|        | POST     | folders/{id}/tasks/{task_id}/edit |                  | App\Http\Controllers\TaskController@edit                               | web          |
|        | GET|HEAD | login                             | login            | App\Http\Controllers\Auth\LoginController@showLoginForm                | web,guest    |
|        | POST     | login                             |                  | App\Http\Controllers\Auth\LoginController@login                        | web,guest    |
|        | POST     | logout                            | logout           | App\Http\Controllers\Auth\LoginController@logout                       | web          |
|        | POST     | password/email                    | password.email   | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web,guest    |
|        | GET|HEAD | password/reset                    | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest    |
|        | POST     | password/reset                    | password.update  | App\Http\Controllers\Auth\ResetPasswordController@reset                | web,guest    |
|        | GET|HEAD | password/reset/{token}            | password.reset   | App\Http\Controllers\Auth\ResetPasswordController@showResetForm        | web,guest    |
|        | GET|HEAD | register                          | register         | App\Http\Controllers\Auth\RegisterController@showRegistrationForm      | web,guest    |
|        | POST     | register                          |                  | App\Http\Controllers\Auth\RegisterController@register                  | web,guest    |
+--------+----------+-----------------------------------+------------------+------------------------------------------------------------------------+--------------+

パスワード一致確認

続いてのポイントはパスワード一致確認機能です。Laravel には confirmed ルールというバリデーションルールが実装されています。このルールはある項目(仮に abc とします)とその項目名 + _confirmation という名前の項目(abc_confirmation)の入力値が一致することを検証します。

<input type="password" class="form-control" id="password-confirm" name="password_confirmation">

確認用のパスワード欄が password_confirmation という名前であるのはこのためです。

コントローラー

会員登録機能を受け持つのは app/Http/Controllers/Auth/RegisterController.php です。一箇所プロパティの値を書き換える必要があります。

RegisterController.php
protected $redirectTo = '/';

$redirectTo は登録に成功した後のリダイレクト先です。

このように Laravel の認証機能は、そのままでも十分使えるデフォルト実装が組み込まれた上で、簡単にカスタマイズできるように作られています。

ここまでできたらブラウザから確認してみましょう。会員登録画面の URL は /register です。いかがでしょうか?登録が正常に行われてホームページに遷移しましたか?データベースクライアントからユーザーデータの行が追加されていることも確認しましょう。

入力エラーメッセージの日本語化

会員登録処理にもバリデーションが定義されていますので、メッセージを日本語化しましょう。

コントローラー

RegisterControllervalidator メソッドでバリデーションが定義されています。FormRequest クラスを使わずに、このようにコントローラーの中で Validator クラスの make メソッドからバリデーション定義を作成する方法もあります。

RegisterController.php
protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:6|confirmed',
    ], [], [
        'name' => 'ユーザー名',
        'email' => 'メールアドレス',
        'password' => 'パスワード',
    ]);
}

make メソッドの第一引数は検証するデータ、第二引数がルール定義、第三引数がメッセージ定義、第四引数が項目名定義です。メッセージは validation.php で定義するのでからの配列を渡し、第四引数で日本語の項目名を定義しています。

ここで新しく登場した unique ルールは、実際にデータベースの内容を参照してすでに使用されている値かどうかを確かめるルールです。ルールの引数である users は参照するテーブル名です。'email' => 'unique:users' は、email の入力値は users テーブルの email カラムで使われていない値でなければいけないという意味になります。

メッセージファイル

続いてメッセージファイルを日本語化します。
resources/lang/jp/validation.php のうち、以下の項目を編集してください。

validation.php
'confirmed'            => ':attribute が確認欄と一致していません。',
'email'                => ':attribute には有効な形式のメールアドレスを入力してください。',
'min'                  => [
    // 略
    'string'  => ':attribute は:min文字以上で入力してください。',
    // 略
],
'string'               => ':attribute には文字を入力してください。',
'unique'               => '入力いただいた :attribute はすでに使用されています。',

会員登録フォームにいろいろな不正値を入力してみてエラーメッセージが日本語化されていることを確かめてください。

ログイン機能

次にログイン機能を実装します。

テンプレート

resources/views/auth/login.blade.php を以下の内容で作成してください。

@extends('layout')

@section('content')
  <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">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('login') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password" />
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-primary">送信</button>
              </div>
            </form>
          </div>
        </nav>
        <div class="text-center">
          <a href="{{ route('password.request') }}">パスワードの変更はこちらから</a>
        </div>
      </div>
    </div>
  </div>
@endsection

コントローラー

会員登録のときと同じように、ログインに成功したあとの遷移先を指定します。

app/Http/Controllers/Auth/LoginController.php を編集しましょう。

LoginController.php
protected $redirectTo = '/';

メッセージの日本語化

ログインに失敗したときのエラーメッセージを日本語化します。

ログインエラーメッセージは auth.php で管理されています。英語版を日本語版のディレクトリにコピーして、faild キーの値を編集しましょう。

$ cp ./resources/lang/en/auth.php ./resources/lang/jp
auth.php
'failed' => 'メールアドレスまたはパスワードに誤りがあります。',

これでログイン機能は実装完了です。簡単ですね!

ではブラウザから確認してみましょう。先ほど会員登録機能の実装で作ったユーザーがログインできたでしょうか?

🎱 🎱 🎱

第8章はこれでおしまいです。長くなってしまいそうなので、ユーザーごとのフォルダの管理やログアウトやパスワード再設定については次の章に譲ることにしました。

ここまでのソースコードはリポジトリ(chapter08 ブランチ)を参照してください。

ユーザーが自分のフォルダのタスクだけを閲覧できる機能などは次の章で実装します。

連載記事