menu-icon

Laravel 6で問い合わせフォームを作ってみる1(フォームの基本部分)

Laravel 6 を使って、問い合わせフォームのサンプルを作ってみます。仕様は以下の通り。

  • 画面は入力画面、確認画面、完了画面の3つ。
  • 入力欄は名前(最大20文字)、Eメール(RFC準拠)、メッセージ(最大1024文字)の3つ。いずれも必須。
  • 送信された問い合わせデータは、Slackの指定したチャンネルに投稿する。
  • Bot対策にreCAPTCHA v3を使用する。

長くなるので、3回に分割して解説します。

初回はフォームの基本的な部分を実装します。

自分の環境

  • Ubuntu 18.04
  • PHP 7.4
  • Laravel 6

作る

フォームリクエスト

コントローラをすっきりさせるために、フォームリクエストを使います。雛型をartisanコマンドで生成します。

$ php artisan make:request InquiryRequest

app/Http/Requests/InquiryRequest.php が生成されたと思います。実装していきます。

app/Http/Requests/InquiryRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class InquiryRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'      => ['required', 'max:20'],
            'email'     => ['required', 'email:rfc'],
            'message'   => ['required', 'max:1024'],
        ];
    }

    /**
     * Get custom messages for validator errors.
     *
     * @return array
     */
    public function messages()
    {
        return [
            'required'  => ':attributeは必須です。',
            'max'       => ':attributeは最大:max文字で入力してください。',
            'rfc'       => '正しいメールアドレスを入力してください。',
        ];
    }

    /**
     * Get custom attributes for validator errors.
     *
     * @return array
     */
    public function attributes()
    {
        return [
            'name'      => 'お名前',
            'email'     => 'メールアドレス',
            'message'   => 'メッセージ',
        ];
    }
}

name, email, message に対する検証を行うフォームリクエストを実装しました。後でreCaptchaのトークンの検証の際、再登場します。

なお簡略化のため messages() メソッドでカスタムエラーメッセージを指定していますが、今回のように属性名を指定しない場合、言語ファイルで指定する方が良いと思います。

コントローラ

コントローラの雛型もartisanコマンドで生成します。

$ php artisan make:controller InquiryController

app/Http/Controllers/InquiryController.php が生成できたら、実装していきます。

app/Http/Controllers/InquiryController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\InquiryRequest;
use Illuminate\Http\Request;

class InquiryController extends Controller
{
    private const FORM_DATA_KEY = 'inquiry.form';

    public function show()
    {
        return view('inquiry.index');
    }

    public function confirm(InquiryRequest $request)
    {
        $form_data = $request->validated();
        $request->session()->put(self::FORM_DATA_KEY, $form_data);

        return view('inquiry.confirm', $form_data);
    }

    public function finish(Request $request)
    {
        if (!$request->session()->has(self::FORM_DATA_KEY)) {
            return redirect()->route('inquiry');
        }

        $form_data = $request->session()->pull(self::FORM_DATA_KEY);

        // ここでSlack送信処理

        return view('inquiry.finish');
    }
}

今回は3つのアクションメソッドを実装しています。

  • show() … 入力画面を表示する。
  • confirm() … 送信された入力を検証し、OKならフォームデータをセッションに保存し、確認画面を表示する。NGなら入力画面へ戻す。
  • finish() … セッションに保存されたデータをSlackへ送信する(予定)。その後、完了画面を表示する。

ルーティング

routes/web.php に以下を追加します。

routes/web.php
Route::get('/inquiry', 'InquiryController@show')->name('inquiry');
Route::post('/inquiry/confirm', 'InquiryController@confirm');
Route::post('/inquiry/finish', 'InquiryController@finish');

ビュー

resource/views下に4つのビューファイルを用意します。

  • layouts/
    • app.blade.php … 共通用
  • inquiry/
    • index.blade.php … 入力画面用
    • confirm.blade.php … 確認画面用
    • finish.blade.php … 完了画面用

layouts/app.blade.php

他のビューのベースとなるビューです。

layouts/app.blade.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="{{ asset('/css/inquiry.css') }}" rel="stylesheet">
    <title>Simple Form - @yield('title')</title>
    @section('script')
    @show
  </head>
  <body>
    <div class="container">
      @yield('content')
    </div>
  </body>
</html>

inquiry/index.blade.php

入力画面です。後でreCAPTCHAの処理が入るのでjsでsubmitしています。

inquiry/index.blade.php
@extends('layouts.app')

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

@section('script')
<script>
  function onClick(e) {
    document.getElementById("contactform").submit();
  }
</script>
@endsection

@section('content')
  @error('token')
    <div class="alert alert-danger">{{ $message }}</div>
  @enderror

  <form method="POST" id="contactform" action="/inquiry/confirm">
    @csrf
    <table class="table-form">
      <tr>
        <th>お名前</th>
        <td>
          <input type="text" name="name" value="{{ old('name') }}"/>
          @error('name')
            <div class="alert alert-danger">{{ $message }}</div>
          @enderror
        </td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td>
          <input type="email" name="email" value="{{ old('email') }}"/>
          @error('email')
            <div class="alert alert-danger">{{ $message }}</div>
          @enderror
        </td>
      </tr>
      <tr>
        <th>メッセージ</th>
        <td>
          <textarea name="message">{{ old('message') }}</textarea>
          @error('message')
            <div class="alert alert-danger">{{ $message }}</div>
          @enderror
        </td>
      </tr>
    </table>
    <div class="button-area">
      <button type="button" class="go" onclick="onClick(event)">確認する</button>
    </div>
  </form>
@endsection

inquiry/confirm.blade.php

確認画面です。簡素にするためフォーム送信時の制御はしてないです。

inquiry/confirm.blade.php
@extends('layouts.app')

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

@section('content')
  <form method="POST" action="/inquiry/finish">
    @csrf
    <table class="table-form">
      <tr>
        <th>お名前</th>
        <td>{{ $name }}</td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td>{{ $email }}</td>
      </tr>
      <tr>
        <th>メッセージ</th>
        <td>{{ $message }}</td>
      </tr>
    </table>
    <div class="button-area">
      <button type="button" class="back" onclick="javascript:window.history.back(-1);return false;">戻る</button>
      <button type="submit" class="go">送信する</button>
    </div>
  </form>
@endsection

inquiry/finish.blade.php

すごくシンプルな完了画面です。

inquiry/finish.blade.php
@extends('layouts.app')

@section('title', 'お問合わせ送信完了')

@section('content')
  <p>お問合わせは正常に送信されました。<br />ありがとうございました。</p>
@endsection

CSSの準備

動きを試すだけであればなくても大丈夫ですが、さすがに寂しいです。それっぽいものを public/css/inquiry.css に用意してください。以下は参考です。

public/css/inquiry.css
*, *:before, *:after {
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
       -o-box-sizing: border-box;
      -ms-box-sizing: border-box;
          box-sizing: border-box;
}
html, body{
  margin: 0;
  padding: 0;
  width:100%;
}

html {
  font-size: 18px;
}

.container {
  max-width: 960px;
  min-width: 360px;
  margin-right: auto;
  margin-left: auto;
}

.alert-danger {
  color: red;
}

.table-form {
  width: 100%;
  border-collapse: collapse;
}
.table-form tr {
  border-top: 1px #f0f0f0 solid;
}
.table-form tr:first-child {
  border-top: none;
}
.table-form th, .table-form td {
  padding: 1rem 0.5rem;
}
.table-form th {
  width: 25%;
  text-align: right;
}
.table-form td {
  width: 75%;
}
.table-form input, .table-form textarea {
  border: 1px #69adce solid;
  padding: 0.5rem 0.3rem;
  border-radius: 4px;
  width: 100%;
}
.table-form textarea {
  resize: none;
  height: 10rem;
}
.table-form input:focus, .table-form textarea:focus {
  background: #EEFFFF;
  box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5);
}
.button-area {
  width: 100%;
  text-align: center;
}
.button-area button {
  display: inline-block;
  margin: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  font-size: 1.2rem;
  box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.5);
  height: 3rem;
  width: 10rem;
}
.button-area button.go {
  background: #4C9ED9;
  color: #fff;
}
.button-area button.back {
  background: #bfbfbf;
  color: #fff;
}

@media (max-width: 959px) {
  .table-form th, .table-form td {
    display: block;
    width: 100%;
    text-align: left;
  }

  .table-form th {
    padding-bottom: 0.2rem;
  }
  .table-form td {
    padding-top: 0.2rem;
  }
}

動かしてみる

ここまで来たら、触れるものはできているはずです。ブラウザで自分のホスト名/inquiryへアクセスしてみてください。入力画面が表示されます。

バリデーションも問題なく動きます。

適切に入力していれば、こちらの確認画面が表示されます。

確認画面で「送信する」を押すと完了画面へ遷移します。現状だと何もやってないですが、本来はこのタイミングで問い合わせ内容がSlackへ送信されます。

ということで、フォームの基本部分ができました。次回は入力された内容をSlackへ送信する部分の処理を実装します。