Laravel 5で確認画面付き問い合わせフォームを作る

確認画面が好きなのは日本人だけなのか、調べてみてもLaravelで確認画面付きフォームを作る例が少ない感じに見えたので、自分がいつもやってる方法を紹介してみる。

## 前準備

* [エラーメッセージ等の日本語化](http://qiita.com/ponko2/items/f2f59b43dae1561ceb50#%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8C%96)
* [Laravel Collectiveの設定](http://qiita.com/ponko2/items/f2f59b43dae1561ceb50#laravel-collective%E3%81%AE%E8%A8%AD%E5%AE%9A)
* [Laravel 5 Repositoriesの設定](http://qiita.com/ponko2/items/f2f59b43dae1561ceb50#laravel-5-repositories%E3%81%AE%E8%A8%AD%E5%AE%9A)

[Laravel 5 Repositories](https://github.com/andersao/l5-repository)は使わなくても作れるけれど、使ったほうがコントローラーがスッキリするのでオススメ。


## 雛形作成

`artisan`で雛形を作成する。

```console
php artisan make:controller --resource ContactController
php artisan make:request ContactRequest
php artisan make:entity Contact
php artisan make:migration create_contacts_table
```

## [Database Migration](https://laravel.com/docs/5.2/migrations)

マイグレーションをこんな感じに編集して

```php:database/migrations/2015_12_01_000000_create_contacts_table.php
<?php

use Illuminate\Database\Migrations\Migration;

class CreateContactsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('contacts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email');
            $table->string('subject');
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('contacts');
    }
}
```

`artisan`でテーブルを作成する

```console
$ php artisan migrate
```

## [Mass Assignment](https://laravel.com/docs/5.2/eloquent#mass-assignment)

更新するカラムを`$fillable`に指定する。適切に設定しておかないと`MassAssignmentException`を吐く。

```php:app/Entities/Contact.php
<?php

namespace App\Entities;

use Illuminate\Database\Eloquent\Model;
use Prettus\Repository\Contracts\Transformable;
use Prettus\Repository\Traits\TransformableTrait;

class Contact extends Model implements Transformable
{
    use TransformableTrait;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'subject',
        'content',
    ];
}
```

## [Routing](https://laravel.com/docs/5.2/controllers#restful-resource-controllers)

`php artisan make:controller --resource`で作成される[RESTful Resource Controllers](https://laravel.com/docs/5.2/controllers#restful-resource-controllers)はこんな感じにRoutingを設定できる。

```php:app/Http/routes.php
<?php

Route::resource('contacts', 'ContactController', ['only' => ['index', 'store']]);
```

今回は`index`と`store`しか使わないため、そのように指定している。

## [Form Request Validation](https://laravel.com/docs/5.2/validation#form-request-validation)

[Form Request Validation](https://laravel.com/docs/5.2/validation#form-request-validation)を使うと、値の検証処理を切り出せるのでコントローラーがすっきりする。

### お問い合わせ画面のRequest
```php:app/Http/Requests/ContactRequest.php
<?php

namespace App\Http\Requests;

class ContactRequest extends Request
{
    use ConfirmRequestTrait;

    /**
     * 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 [
            'name'    => 'required',
            'email'   => 'required|email',
            'subject' => 'required',
            'content' => 'required',
        ];
    }

    /**
     * Set custom messages for validator errors.
     *
     * @return array
     */
    public function messages()
    {
        return [
            //
        ];
    }

    /**
     * Set custom attributes for validator errors.
     *
     * @return array
     */
    public function attributes()
    {
        return [
            'name'    => 'お名前',
            'email'   => 'メールアドレス',
            'subject' => '件名',
            'content' => '内容',
        ];
    }
}
```

`use ConfirmRequestTrait`を追加することで、確認画面付きのフォームにしている。`authorize`メソッドの返り値を`true`にしておかないと、403エラーになるので注意。[Form Request Validation](https://laravel.com/docs/5.2/validation#form-request-validation)の**Authorizing Form Requests**に詳しい説明が書いてある。

`attributes`でエラーメッセージの項目名を変更することができてとても便利なのだけれど、これを使ったコードをあげている人をあまり見ないのはなんでだろう？

### 確認画面の共通処理

**After Validation Hook**を使って確認画面用フラグの制御をしている。もっとスッキリ書く方法があるかも。

```php:app/Http/Requests/ConfirmRequestTrait.php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;

trait ConfirmRequestTrait
{
    /**
     * Set custom messages for validator errors.
     *
     * @param \Illuminate\Contracts\Validation\Factory $factory
     *
     * @return \Illuminate\Contracts\Validation\Validator
     */
    public function validator($factory)
    {
        // 値検証前の処理
        if (method_exists($this, 'beforeValidate')) {
            $this->beforeValidate();
        }

        // 確認画面用フラグのバリデーションを追加
        $rules = array_merge($this->rules(), [
            'confirming' => 'required|accepted',
        ]);

        $validator = $factory->make(
            $this->all(),
            $rules,
            $this->messages(),
            $this->attributes()
        );

        $validator->after(function ($validator) {
            $failed = $validator->failed();

            // 確認画面用フラグのバリデーションを除外
            unset($failed['confirming']);

            // 確認画面用フラグ以外にエラーが無い場合は確認画面を表示
            if (count($failed) === 0) {
                $this->merge(['confirming' => 'true']);
            }

            // 値検証後の処理
            if (method_exists($this, 'afterValidate')) {
                $this->afterValidate($validator);
            }
        });

        return $validator;
    }

    /**
     * Format the errors from the given Validator instance.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     *
     * @return array
     */
    protected function formatErrors(Validator $validator)
    {
        $errors = parent::formatErrors($validator);

        // 確認画面用フラグのエラーメッセージを削除
        unset($errors['confirming']);

        return $errors;
    }
}
```

## Repository Service Provider

`make:entity`で生成された`ContactRepository`を登録する。

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

/**
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $repositories = [
            \App\Repositories\ContactRepository::class,
        ];

        foreach ($repositories as $repository) {
            $this->app->bind($repository, $repository.'Eloquent');
        }
    }
}
```


## [Blade Templates](https://laravel.com/docs/5.2/blade)

### 共通部分

```php:resources/views/layouts/master.blade.php
<!DOCTYPE html>
<html lang="{{ App::getLocale() }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>@yield('title') - App Name</title>
        @section('styles')
            <link rel="stylesheet" href="{{ elixir('css/app.css') }}">
        @show
        <!--[if lt IE 9]>
            <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
            <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
    </head>
    <body class="@yield('body-class')">
        <div class="container">
            @yield('content')
        </div>
        @section('scripts')
            <script src="{{ elixir('js/main.js') }}"></script>
        @show
    </body>
</html>
```

### フォーム画面

```php:resources/views/contacts/index.blade.php
@extends('layouts.master')

@section('title', 'お問い合わせ')

@section('content')
    @if(count($errors) > 0)
        <div class="alert alert-danger">
            <ul>
                @foreach($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif
    <div class="panel panel-default">
        <div class="panel-heading">
            <h1 class="panel-title">お問い合わせ</h1>
        </div>
        <div class="panel-body">
            {!! Form::open(['route' => ['contacts.store'], 'method' => 'post']) !!}
                <input type="hidden" name="confirming" value="{{ old('confirming', 'false') }}">
                <div class="form-group required {{ $errors->has('name') ? 'has-error' : '' }}">
                    <label class="control-label" for="name">お名前</label>
                    @if(old('confirming', 'false') === 'false')
                        <input type="text" class="form-control" name="name" value="{{ old('name') }}">
                    @else
                        <p class="form-control-static">{{ old('name') }}</p>
                        <input type="hidden" name="name" value="{{ old('name') }}">
                    @endif
                    @if($errors->has('name'))
                        <p class="help-block">{{ $errors->first('name') }}</p>
                    @endif
                </div>
                <div class="form-group required {{ $errors->has('email') ? 'has-error' : '' }}">
                    <label class="control-label" for="email">メールアドレス</label>
                    @if(old('confirming', 'false') === 'false')
                        <input type="email" class="form-control" name="email" value="{{ old('email') }}">
                    @else
                        <p class="form-control-static">{{ old('email') }}</p>
                        <input type="hidden" name="email" value="{{ old('email') }}">
                    @endif
                    @if($errors->has('email'))
                        <p class="help-block">{{ $errors->first('email') }}</p>
                    @endif
                </div>
                <div class="form-group required {{ $errors->has('subject') ? 'has-error' : '' }}">
                    <label class="control-label" for="subject">件名</label>
                    @if(old('confirming', 'false') === 'false')
                        <input type="text" class="form-control" name="subject" value="{{ old('subject') }}">
                    @else
                        <p class="form-control-static">{{ old('subject') }}</p>
                        <input type="hidden" name="subject" value="{{ old('subject') }}">
                    @endif
                    @if($errors->has('subject'))
                        <p class="help-block">{{ $errors->first('subject') }}</p>
                    @endif
                </div>
                <div class="form-group required {{ $errors->has('content') ? 'has-error' : '' }}">
                    <label class="control-label" for="content">内容</label>
                    @if(old('confirming', 'false') === 'false')
                        <textarea type="text" class="form-control" name="content" rows="10">{{
                            old('content')
                        }}</textarea>
                    @else
                        <p class="form-control-static">{!! nl2br(e(old('content'))) !!}</p>
                        <input type="hidden" name="content" value="{{ old('content') }}">
                    @endif
                    @if($errors->has('content'))
                        <p class="help-block">{{ $errors->first('content') }}</p>
                    @endif
                </div>
                <div class="form-group text-center">
                    @if(old('confirming', 'false') === 'false')
                        <button type="submit" class="btn btn-primary">確認</button>
                    @else
                        <button type="submit" name="action" value="post" class="btn btn-primary">送信</button>
                        <button type="submit" name="action" value="back" class="btn btn-default">戻る</button>
                    @endif
                </div>
            {!! Form::close() !!}
        </div>
    </div>
@endsection
```

`<input type="hidden" name="confirming" value="{{ old('confirming', 'false') }}">` で確認画面の切替をする。

確認画面では「送信」と「戻る」で処理が別れるため、ボタンに`name`と`value`を指定して分岐させる。

この例では1つのテンプレートにまとめてしまったけれど、入力パーツ毎に細かいテンプレートを作って使い回せば綺麗に書けると思う。

### 送信ボタンダブルクリックでの二重送信防止

こんな感じのJavaScriptを仕込んでおくといいと思う。`submit`を単純に無効化してしまうと、戻るボタンが効かなくなるので注意。

```js
$('form :submit').click(function (event) {
  var TIMEOUT = 10000;
  var target  = event.target;
  var $form   = $(target).closest('form');
  var $submit = $form.find(':submit');

  // clickしたsubmitの値をhiddenに保存
  var $hidden = $('<input/>', {
    type: 'hidden',
    name: target.name,
    value: target.value
  }).appendTo($form);

  event.preventDefault();
  event.stopPropagation();

  // 全てのsubmitを無効化
  $submit.prop('disabled', true);

  // 時間経過でsubmitの無効化を解除
  setTimeout(function () {
    $hidden.remove();
    $submit.prop('disabled', false);
  }, TIMEOUT);

  $form.submit();
});
```

### 投稿完了画面

```php:resources/views/contacts/thanks.blade.php
@extends('layouts.master')

@section('title', 'お問い合わせ')

@section('content')
    <div class="panel panel-default">
        <div class="panel-heading">
            <h1 class="panel-title">お問い合わせ</h1>
        </div>
        <div class="panel-body">
            <div class="well well-lg text-center">お問い合わせありがとうございました</div>
        </div>
    </div>
@endsection
```

## [Controllers](https://laravel.com/docs/5.2/controllers#restful-resource-controllers)

`store`メソッドの引数に`ContactRequest`を指定すると[Form Request Validation](https://laravel.com/docs/5.2/validation#form-request-validation)で値の検証ができるので、そのままデータベースに突っ込む。

ここでは完了ページでブラウザリロードされた時に二重送信されないようにトークンの再発行も行っている。

```php:app/Http/Controllers/ContactController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\ContactRequest;
use App\Repositories\ContactRepository;

class ContactController extends Controller
{
    /**
     * @var ContactRepository
     */
    protected $contacts;

    /**
     * コンストラクタ
     *
     * @param ContactRepository $contacts
     */
    public function __construct(ContactRepository $contacts)
    {
        $this->contacts = $contacts;
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return view('contacts.index');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param ContactRequest $request
     *
     * @return \Illuminate\Http\Response
     */
    public function store(ContactRequest $request)
    {
        // 確認画面で戻るボタンが押された場合
        if ($request->get('action') === 'back') {
            // 入力画面へ戻る
            return redirect()
                ->route('contacts.index')
                ->withInput($request->except(['action', 'confirming']));
        }

        // データベースに登録
        $this->contacts->create($request->all());

        // ブラウザリロード等での二重送信防止
        $request->session()->regenerateToken();

        // 完了画面を表示
        return view('contacts.thanks');
    }
}
```

## ソースコード

https://github.com/ponko2/laravel-starter-kit/tree/contacts

## 画面サンプル

### 入力画面

![入力画面.png](https://qiita-image-store.s3.amazonaws.com/0/109/ab5a0a97-e458-2061-ce10-fb5de229b990.png)

### エラー画面

![エラー画面.png](https://qiita-image-store.s3.amazonaws.com/0/109/cbb37a37-5128-1a4b-be7f-54ac922ee38a.png)

### 確認画面

![確認画面.png](https://qiita-image-store.s3.amazonaws.com/0/109/bef7d9c4-ddb2-cb31-14d8-952a0be9faca.png)

### 完了画面

![完了画面.png](https://qiita-image-store.s3.amazonaws.com/0/109/ae870d72-c317-96c3-d034-ab04e11f3138.png)