現在Laravelの教材を鋭意制作中です!完成まで今しばらくお待ちください

#20 Formの実装

前章では、viewについて学んできました。

今回はこのアプリの核ともいえるお問い合わせフォームを作成していきます。

1. pullとブランチの作成

まずは、毎回恒例のやつですね。作業に入る前に前回mergeしたメインブランチを更新しておきましょう。

# ブランチをpullする
git pull origin main

以上で完了です。mainブランチが前章で作成されたブランチと同じになれば、次に進みます。

git switch -C make-form

今回はフォームを作成するので、安直ですがmake-formと言う名前のブランチを作成しました。ブランチ名は任意となりますので、お好きなブランチ名を付けて構いません。

2.お問い合わせフォーム

ではブランチを作成したら、お問い合わせフォームを作成していきましょう。

まずは、一気に作成するお問い合わせフォームのページをご覧ください。

@extends('layouts.app')

@section('title', 'お問い合わせフォーム')

@section('content')
<h2>お問い合わせフォーム</h2>

<form action="/contact/confirm" method="POST">
@csrf

<!-- 名前 -->
<div>
  <label for="name">お名前:</label><br>
  <input type="text" id="name" name="name">
</div><br>

<!-- メールアドレス -->
<div>
  <label for="email">メールアドレス:</label><br>
  <input type="email" id="email" name="email">
</div><br>

<!-- 年齢(セレクトボックス) -->
<div>
  <label for="age">年齢:</label><br>
  <select name="age" id="age" required>
    <option value="">選択してください</option>
    @for ($i = 18; $i <= 65; $i++)
      <option value="{{ $i }}">{{ $i }}歳</option>
    @endfor
  </select>
</div><br>

<!-- 性別(ラジオボタン) -->
<div>
  <input type="radio" id="gender_male" name="gender" value="男性" required>
  <label for="gender_male">男性</label>
  <input type="radio" id="gender_female" name="gender" value="女性">
  <label for="gender_female">女性</label>
  <input type="radio" id="gender_other" name="gender" value="その他">
  <label for="gender_other">その他</label>
</div><br>

<!-- 興味のあるサービス領域(チェックボックス) -->
<div>
  <label>興味のある分野:</label><br>
  <label><input type="checkbox" name="interests[]" value="Webサイト制作"> Webサイト制作</label><br>
  <label><input type="checkbox" name="interests[]" value="システム開発"> システム開発</label><br>
  <label><input type="checkbox" name="interests[]" value="WordPress運用"> WordPress運用</label><br>
  <label><input type="checkbox" name="interests[]" value="Laravel構築"> Laravel構築</label><br>
  <label><input type="checkbox" name="interests[]" value="SEO相談"> SEO相談</label>
</div><br>

<!-- 本文 -->
<div>
  <label for="message">お問い合わせ内容:</label><br>
  <textarea name="message" id="message" rows="5"></textarea>
</div><br>

<!-- 送信ボタン -->
<button type="submit">内容を確認する</button>
</form>
@endsection

コードの解説

それではコードの解説に移ります。フォームの塊ごとに解説を行います。

bladeテンプレート

@extends('layouts.app')

@section('title', 'お問い合わせフォーム')

@section('content')

前章の「共通化してみよう」でも解説しましたが、 @extends('layouts.app') は「このビューは layouts/app.blade.php をベースにして作られていますよ」という レイアウト継承の宣言です。

@section('title', 'お問い合わせフォーム') は HTMLの<title> タグに表示されるページタイトルを設定しています。レイアウト側に @yield('title') があることで、そこに「お問い合わせフォーム」が差し込まれます。

@section('content')@endsection の範囲内はページの主な中身(本文)を定義する領域で、レイアウト側に@yield('content') があることで、ここのHTMLがそこに挿入されます。

Formタグ

<form action="/contact/confirm" method="POST">

このフォームタグは、ユーザーが入力した情報を「確認画面」に送信するための仕組みです。

action="/contact/confirm"は送信先URLを指定する属性で、このフォームを送信すると、Laravelのルーティングで定義された /contact/confirm に対してリクエストが飛びます。

リクエストが/contact/confirmに飛ぶので、対応するコントローラーの confirm() メソッドが呼び出される流れです。

method="POST"はHTTPメソッドを指定する属性(GETではなくPOSTを使用)です。

フォーム入力のような機密性があるデータ送信には POST が必須であり、Laravelでは POST で送ることで、バリデーションや保存処理が自然な流れで実装できます。

CSRF

@csrf

@csrf は Laravelが提供するBlade命令で、フォームに「CSRFトークン(セキュリティ用の隠し情報)」を自動で埋め込む命令です。

補足

CSRF = Cross-Site Request Forgery(クロスサイトリクエストフォージェリ)
ユーザーが意図しないリクエストを第三者が送信することで、情報改ざんや不正操作を引き起こす攻撃

実はフォーム内で @csrf を記述すると、HTMLには以下のような <input type="hidden"> が自動生成されます。

<input type="hidden" name="_token" value="ランダムなトークン文字列">

Laravelではフォーム送信のたびに、サーバー側と一致する「トークン」が送られるように仕組まれていて、このトークンが一致しなければそのリクエストを拒否します。

名前入力フィールド

<div><label for="name">お名前:</label><br>
<input type="text" id="name" name="name">
</div><br>

<label for="name">はフォームに「何を入力するか」を示すラベル(見出し)です。

for="name" により、次の inputid="name" と紐づいており、ラベルをクリック(お名前)すると対応する入力欄にフォーカスが当たるなど、アクセシビリティ向上にもつながります。

<input type="text" id="name" name="name">はユーザーが名前を入力するテキストフィールドです。

id="name" はラベルとの対応付け、name="name" はこのフィールドのデータ名で、送信時に $request->name として取得されます。

メールアドレス入力フィールド

<div>
<label for="email">メールアドレス:</label><br>
<input type="email" id="email" name="email">
</div>

<label for="email">はフォームに「ここはメールアドレスを入力する欄ですよ」と伝えるラベル(見出し)です。

for="email" によって、次の inputid="email" と紐づいており、ラベルをクリック(お名前)すると対応する入力欄にフォーカスが当たるなど、アクセシビリティ向上にもつながります。

<input type="email">は入力欄の型として type="email" を指定することで、HTML5の機能により「メール形式(例:name@example.com)」の入力チェックが可能になります。

また、スマホ等ではメール用キーボード(@入り)を自動表示されるので、非常に便利になりました。

id="email" はラベルとの対応付け、name="email" は送信時のキー名であり、コントローラーで $request->email などで取得できます。

年齢セレクトボックス

<div>
  <label for="age">年齢:</label><br>
  <select name="age" id="age">
    <option value="">選択してください</option>
    @for ($i = 18; $i <= 65; $i++)
      <option value="{{ $i }}">{{ $i }}歳</option>
    @endfor
  </select>
</div><br>

<label for="age">はフォームに「ここはメールアドレスを入力する欄ですよ」と伝えるラベル(見出し)です。

for="age" によって、次の selectselect id="age" と紐づいており、ラベルをクリック(お名前)するとセレクトボックスにフォーカスが移るなど、アクセシビリティ向上にもつながります。

<select name="age" id="age">はユーザーがプルダウンで年齢を選択できるセレクトボックスです。

required 属性によって未選択のまま送信できないようになっており、バリデーション前でも最低限の制約が効いています。(これは次章にて解説します)

name="age" によって送信された値は Request $request から $request->age で取得可能になります。(Request $requestはコントローラーで登場します)

このセレクトボックス部分では、@for ループによる動的な選択肢の作成があります。

@for ($i = 18; $i <= 65; $i++)
  <option value="{{ $i }}">{{ $i }}歳</option>
@endfor

Laravel Blade構文の @for ループにより、18歳〜65歳までを自動で生成しており、年齢の範囲を変更したい場合は $i = 18<= 65 を調整するだけでOKとなります。

性別選択ラジオボタン

<div>
  <input type="radio" id="gender_male" name="gender" value="男性" required>
  <label for="gender_male">男性</label>
  <input type="radio" id="gender_female" name="gender" value="女性">
  <label for="gender_female">女性</label>
  <input type="radio" id="gender_other" name="gender" value="その他">
  <label for="gender_other">その他</label>
</div><br>

<label for="gender_male">はフォームに「ここはメールアドレスを入力する欄ですよ」と伝えるラベル(見出し)です。

for="gender_male" によって、前の inputid="gender_male" と紐づいており、ラベルをクリック(お名前)するとラジオボタンにフォーカスが移るなど、アクセシビリティ向上にもつながります。

<input type="radio" id="gender_male" name="gender" value="男性" required>は性別選択のラジオボタンのうち「男性」を表す入力欄を定義するものです。

type="radio"はラジオボタン形式の入力欄で、複数の選択肢の中から「1つだけ選ぶ」場合に使用されます。

同じ name を持つラジオボタン同士が1グループとして認識され、今回は name="gender"と3つある性別のラジオボタンすべてがこの名前を共有しています。

name="gender"によって「どれか1つを選ぶグループ」として機能し、フォーム送信時には gender=男性 などの形式で1つの値が送られます。

value="男性"は「男性」が選ばれた場合に送信される値で、Controllerでは $request->gender'男性' の値として受け取れます。

required属性によって未選択のまま送信できないようになっており、ユーザーが何も選ばずにフォームを送ろうとすると、ブラウザがエラーを表示されます。

ちなみに、1つのラジオボタンに required を付けるだけでグループ全体が「必須扱い」になります(今回は性別のグループすべてが必須)

興味分野チェックボックス

<div>
<label>興味のある分野:</label><br>
  <label><input type="checkbox" name="interests[]" value="Webサイト制作"> Webサイト制作</label><br>
  <label><input type="checkbox" name="interests[]" value="システム開発"> システム開発</label><br>
  <label><input type="checkbox" name="interests[]" value="WordPress運用"> WordPress運用</label><br>
  <label><input type="checkbox" name="interests[]" value="Laravel構築"> Laravel構築</label><br>
  <label><input type="checkbox" name="interests[]" value="SEO相談"> SEO相談</label>
</div>

<input type="checkbox" name="interests[]" value="Webサイト制作"> は、興味分野のチェックボックスのうち「Webサイト制作」を表す入力欄を定義するものです。

type="checkbox"はチェックボックス形式の入力欄で、複数の選択肢の中から好きなだけチェックして選べるようになっています。

ラジオボタンと違って「複数選択OK」なスタイルですが、それぞれに異なる value を持たせることで、何を選んだか明確に記録できます。

name="interests[]"[] を付けることで、Laravelでは配列として受け取れるようになっており、$request->interests で複数値取得可能となっています。

補足

Laravelのフォームでは、チェックボックスに name="interests[]"[] を付けることで、自動的に配列になります。

今回は <label>input を囲むことで、テキスト部分をクリックしてもチェックできる構造です。もう少し構造的に書くと、以下のような形になります。

<label>
  <input type="checkbox" name="interests[]" value="Laravel構築">
  Laravel構築
</label>

先ほどまでは forid を使用していましたが、今回のように input 属性を <label> で囲う形にしても、同じようなアクセシビリティを実現できます。

例えば、これまでと同じようにforid を使用する場合は以下のような形になります。

<div>
<label>興味のある分野:</label><br>
  <input type="checkbox" id="interest_web" name="interests[]" value="Webサイト制作">
  <label for="interest_web">Webサイト制作</label><br>
  <input type="checkbox" id="interest_system" name="interests[]" value="システム開発">
  <label for="interest_system">システム開発</label><br>
  <input type="checkbox" id="interest_wp" name="interests[]" value="WordPress運用">
  <label for="interest_wp">WordPress運用</label><br>
  <input type="checkbox" id="interest_laravel" name="interests[]" value="Laravel構築">
  <label for="interest_laravel">Laravel構築</label><br>
  <input type="checkbox" id="interest_seo" name="interests[]" value="SEO相談">
  <label for="interest_seo">SEO相談</label>
</div><br>

こちらの方がしっかりと書いてあるのでラベルとフォームのつながりが分かりやすい一方で、コードが長くなるというデメリットもありますね。

お問い合わせ本文入力フィールド

<div>
  <label for="message">お問い合わせ内容:</label><br>
  <textarea name="message" id="message" rows="5"></textarea>
</div>

<label for="message">はフォームに「ここはメッセージを入力する欄ですよ」と伝えるラベル(見出し)です。

for="message" によって、後の textareaid="message" と紐づいており、ラベルをクリック(お名前)するとテキストエリアにフォーカスが移るなど、アクセシビリティ向上にもつながります。

<textarea>は複数行のテキスト入力を行うためのタグです。

name="message" により、フォーム送信時には $request->message として取得可能になっています。

また、rows="5" は表示される縦の行数を指定できます。(視認性や入力量に応じて調整可能)

3.コントローラーの実装

フォームを実装したら、コントローラーの実装も行います。

use App\Models\Contact;
use Illuminate\Http\Request;

public function store(Request $request)
{
  $data = $request->all();

  // チェックボックス(配列)を文字列に変換
  $data['interests'] = isset($data['interests'])
    ? implode(',', $data['interests'])
    : null;

  Contact::create($data);

  return redirect('/thanks');
}

コードの解説

use宣言

use App\Models\Contact;
use Illuminate\Http\Request;

ModelのContactモデルを使用します、という意味になります。

メソッド定義

public function store(Request $request)

この store メソッドは、お問い合わせフォームの最終処理(保存)を担当するコントローラー側のアクションです。

 

Request からデータを取得

$data = $request->all();

ユーザーが送信したフォームの値をすべて取得し、$data に格納し、$request->all() によって、POSTされた全フィールドの値を配列形式で取得可能になっています。

仮に以下のようなフォーム入力がされていたとしましょう。

<!-- ユーザーが入力した内容の例 -->
名前: 山田太郎
メール: yamada@example.com
年齢: 28
性別: 男性
興味分野: Webサイト制作, Laravel構築
お問い合わせ内容: サイト制作の見積もりをお願いしたいです

$data に格納される値の例(フォーム送信後)

[
  "name" => "山田太郎",
  "email" => "yamada@example.com",
  "age" => "28",
  "gender" => "男性",
  "interests" => [ // チェックボックスは配列として渡る
    "Webサイト制作",
    "Laravel構築"
  ],
  "message" => "サイト制作の見積もりをお願いしたいです",
  "_token" => "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" // CSRFトークン
]

interests はチェックボックスなので配列形式で格納され、Laravelでは $request->interests でそのまま配列として操作可能であり、さらに後処理で implode() で文字列化できます

_token は CSRF対策用で、@csrf によって自動挿入されたものです。

チェックボックス(配列)を文字列に変換

interests はチェックボックスなので、選択肢が複数ある場合は配列として渡ります。

DB に保存し辛いので、配列から文字列に変換する処理をする必要があります。

$data['interests'] = isset($data['interests'])
  ? implode(',', $data['interests'])
  : null;

isset($data['interests']) は存在確認を行うメソッドで、もしチェックボックスが1つも選ばれていない場合、interests キー自体が存在しないことがあります。

そのため、先に isset($data[‘interests’]) で確認しています。

未選択なら null をセットすることで、保存時にエラーを防ぎ、空データとして扱うことができます。

isset($data['interests'])による確認で、中身がある場合はimplode()関数が発動し、この関数は配列を文字列に変換します。

今回の構文で使われている三項演算子はif文を1行で書ける式で、条件式 ? 真の場合の値 : 偽の場合の値;となっています。if文にすると以下の通りです。

if (isset($data['interests'])) {
  $data['interests'] = implode(',', $data['interests']);
} else {
  $data['interests'] = null;
}

データベース保存処理

Contact::create($data);

Contact モデルを通して、フォームの内容をデータベースに保存できます。

モデル側で $fillable プロパティが正しく設定されている必要ありますが、Eloquentの create() メソッドによって一括で保存できるので、コードが簡潔で見やすいという特徴があります。

サンクスページへのリダイレクト

return redirect('/thanks');

保存処理が終わったら、/thanks ページに自動的に移動し、フォーム送信者に「ありがとうございました」の完了画面を表示します。

なぜここがreturn view('thanks');ではないのかというと、return view('thanks');現在のルート処理のまま、指定されたビューを表示する方法なので、ブラウザのアドレスバーは変わらず /contact(たとえばPOST送信元)などのままになります。

つまり、ユーザーが F5キー を押してデータを再送信した場合、リロードすると同じPOSTリクエストが再送信されてデータが二重登録が発生する可能性があります。

その点redirect() を使えば、GETメソッドに切り替えてサンクス画面が表示されるうえ、ブラウザのアドレスバーも /thanks に切り替わり、ユーザーも「送信完了した」と実感しやすいです。

これでフォームの作成からデータを登録する部分までが完成となります。最後にいつものあれを行いましょう。

4.Git push

以上が完了したら、ここでGitを共有しておきましょう。

git add -A
git commit -m "コメントを入力する"
git push origin make-form

git pushが無事完了したら、GitHub上で操作を行います。

pull requestが来ているので、mergeしましょう。

次章:#21 バリデーションとFormRequestの実装 に進む