【Laravel】お問い合わせフォームを作成する|簡単な掲示板アプリの作成

LaravelでWebアプリの必須機能であるお問い合わせフォームを作成します。入力・確認・完了画面の作り方から、入力値のバリデーション、そしてMailableクラスを使ったメール送信機能までを解説。管理者とユーザーへ自動返信メールを送る方法も学べます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • PHP: 8.3.11
  • Laravel: 11.29.0
  • laravel/breeze: 2.2

サンプルコード

/app/Http/Controllers/ContactController.php

/app/Http/Controllers/ContactController.php
1+<?php
2+
3+namespace App\Http\Controllers;
4+
5+use Illuminate\Http\Request;
6+use Illuminate\Support\Facades\Mail;
7+use App\Mail\ContactMail;
8+
9+class ContactController extends Controller
10+{
11+    /**
12+     * お問い合わせフォームの表示
13+     */
14+    public function index()
15+    {
16+        return view('contact.index');
17+    }
18+
19+    /**
20+     * 確認画面の表示
21+     */
22+    public function confirm(Request $request)
23+    {
24+        $request->validate([
25+            'name' => 'required|string|max:255',
26+            'email' => 'required|email|max:255',
27+            'message' => 'required|string|max:1000',
28+        ]);
29+
30+        // 確認画面を表示
31+        $contactData = $request->all();
32+        return view('contact.confirm', compact('contactData'));
33+    }
34+
35+    /**
36+     * お問い合わせ内容の送信処理
37+     */
38+    public function send(Request $request)
39+    {
40+        $request->validate([
41+            'name' => 'required|string|max:255',
42+            'email' => 'required|email|max:255',
43+            'message' => 'required|string|max:1000',
44+        ]);
45+
46+        $contactData = $request->only('name', 'email', 'message');
47+
48+        // メールの送信(運営者)
49+        Mail::to(config('mail.from.address'))->send(new ContactMail($contactData, 'admin'));
50+
51+        // メールの送信(ユーザー)
52+        Mail::to($request->email)->send(new ContactMail($contactData, 'user'));
53+
54+        return redirect()->route('contact.complete');
55+    }
56+
57+    public function complete()
58+    {
59+        return view('contact.complete');
60+    }
61+}
62

/app/Mail/ContactMail.php

/app/Mail/ContactMail.php
1+<?php
2+
3+namespace App\Mail;
4+
5+use Illuminate\Bus\Queueable;
6+use Illuminate\Contracts\Queue\ShouldQueue;
7+use Illuminate\Mail\Mailable;
8+use Illuminate\Mail\Mailables\Content;
9+use Illuminate\Mail\Mailables\Envelope;
10+use Illuminate\Queue\SerializesModels;
11+
12+class ContactMail extends Mailable
13+{
14+    use Queueable, SerializesModels;
15+
16+    public $contact;
17+    public $role;
18+
19+    /**
20+     * Create a new message instance.
21+     */
22+    public function __construct($contact, $role)
23+    {
24+        $this->contact = $contact;
25+        $this->role = $role;
26+    }
27+
28+    /**
29+     * Get the message envelope.
30+     */
31+    public function envelope(): Envelope
32+    {
33+        return new Envelope(
34+            subject: $this->role === 'admin' ? '新しいお問い合わせがありました' : 'お問い合わせありがとうございます',
35+        );
36+    }
37+
38+    /**
39+     * Get the message content definition.
40+     */
41+    public function content(): Content
42+    {
43+        return new Content(
44+            view: 'emails.contact', // 実際のビューのパスに修正
45+            with: [
46+                'contact' => $this->contact, // 送信内容をビューに渡す
47+                'role' => $this->role
48+            ],
49+        );
50+    }
51+
52+    /**
53+     * Get the attachments for the message.
54+     *
55+     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
56+     */
57+    public function attachments(): array
58+    {
59+        return [];
60+    }
61+}
62

/resources/views/contact/complete.blade.php

/resources/views/contact/complete.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h1>お問い合わせが完了しました</h1>
5+    <p>メールが正常に送信されました。ご連絡ありがとうございます。</p>
6+    <p>今後ともよろしくお願いいたします。</p>
7+    <a href="{{ route('posts.index') }}">掲示板一覧に戻る</a>
8+@endsection
9

/resources/views/contact/confirm.blade.php

/resources/views/contact/confirm.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h2>お問い合わせ確認</h2>
5+
6+    <form action="{{ route('contact.send') }}" method="POST">
7+        @csrf
8+        <input type="hidden" name="name" value="{{ $contactData['name'] }}">
9+        <input type="hidden" name="email" value="{{ $contactData['email'] }}">
10+        <input type="hidden" name="message" value="{{ $contactData['message'] }}">
11+
12+        <div class="mb-3">
13+            <label class="form-label">お名前</label>
14+            <p>{{ $contactData['name'] }}</p>
15+        </div>
16+        <div class="mb-3">
17+            <label class="form-label">メールアドレス</label>
18+            <p>{{ $contactData['email'] }}</p>
19+        </div>
20+        <div class="mb-3">
21+            <label class="form-label">メッセージ</label>
22+            <p>{{ nl2br(e($contactData['message'])) }}</p>
23+        </div>
24+
25+        <button type="button" onclick="history.back()" class="btn btn-secondary">修正</button>
26+        <button type="submit" class="btn btn-primary">送信</button>
27+    </form>
28+@endsection
29

/resources/views/contact/index.blade.php

/resources/views/contact/index.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h2>お問い合わせ</h2>
5+
6+    <form action="{{ route('contact.confirm') }}" method="POST">
7+        @csrf
8+        <div class="mb-3">
9+            <label for="name" class="form-label">お名前</label>
10+            <input type="text" name="name" id="name" class="form-control" value="{{ old('name') }}" required>
11+        </div>
12+        <div class="mb-3">
13+            <label for="email" class="form-label">メールアドレス</label>
14+            <input type="email" name="email" id="email" class="form-control" value="{{ old('email') }}" required>
15+        </div>
16+        <div class="mb-3">
17+            <label for="message" class="form-label">メッセージ</label>
18+            <textarea name="message" id="message" class="form-control" rows="5" required>{{ old('message') }}</textarea>
19+        </div>
20+        <button type="submit" class="btn btn-primary">確認</button>
21+    </form>
22+@endsection
23

/resources/views/emails/contact.blade.php

/resources/views/emails/contact.blade.php
1+<!DOCTYPE html>
2+<html lang="ja">
3+<head>
4+    <meta charset="UTF-8">
5+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6+    <title>お問い合わせ</title>
7+</head>
8+<body>
9+    <h1>{{ $role === 'admin' ? '新しいお問い合わせ' : 'お問い合わせ内容' }}</h1>
10+
11+    <p><strong>お名前:</strong> {{ $contact['name'] }}</p>
12+    <p><strong>メールアドレス:</strong> {{ $contact['email'] }}</p>
13+    <p><strong>メッセージ:</strong></p>
14+    <p>{{ $contact['message'] }}</p>
15+</body>
16+</html>
17

/resources/views/layout.blade.php

/resources/views/layout.blade.php
1     <title>掲示板アプリ</title>
2     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
3     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
4-    {{-- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
5-    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.0.0/dist/umd/popper.min.js"></script>
6-    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> --}}
7 </head>
8 <body>
9     <nav class="navbar navbar-expand-lg bg-primary">
10                             <a class="nav-link text-white" href="{{ route('register') }}">新規登録</a>
11                         </li>
12                     @endif
13+                    <li class="nav-item">
14+                        <a class="nav-link text-white" href="{{ route('contact.index') }}">お問い合わせ</a>
15+                    </li>
16                 </ul>
17             </div>
18         </div>
19

/routes/web.php

/routes/web.php
1 use App\Http\Controllers\PostController;
2 use App\Http\Controllers\CommentController;
3 use App\Http\Controllers\LikeController;
4+use App\Http\Controllers\ContactController;
5 use Illuminate\Support\Facades\Route;
6 
7 Route::get('/', function () {
8 // いいねを投稿するルーティング
9 Route::post('/posts/{post}/like', [LikeController::class, 'toggleLike'])->name('posts.like')->middleware('auth');
10 
11+// お問い合わせフォーム
12+Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
13+Route::post('/contact/confirm', [ContactController::class, 'confirm'])->name('contact.confirm');
14+Route::post('/contact/send', [ContactController::class, 'send'])->name('contact.send');
15+Route::get('/contact/complete', [ContactController::class, 'complete'])->name('contact.complete');
16+
17 require __DIR__.'/auth.php';
18

コード解説

変更点: お問い合わせフォーム用のルーティングを追加

/routes/web.php
1+use App\Http\Controllers\ContactController;
2...
3+// お問い合わせフォーム
4+Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
5+Route::post('/contact/confirm', [ContactController::class, 'confirm'])->name('contact.confirm');
6+Route::post('/contact/send', [ContactController::class, 'send'])->name('contact.send');
7+Route::get('/contact/complete', [ContactController::class, 'complete'])->name('contact.complete');

ここでは、お問い合わせフォーム機能に必要な4つのURL(ルート)を定義しています。routes/web.phpファイルは、ブラウザからのアクセスと、それに対応する処理(コントローラーのメソッド)を結びつける役割を持ちます。

  • /contact (GET): お問い合わせ入力画面を表示します。
  • /contact/confirm (POST): 入力されたデータを検証し、確認画面を表示します。
  • /contact/send (POST): 確認されたデータをメールで送信します。
  • /contact/complete (GET): 送信完了画面を表示します。

name()メソッドは各ルートに名前を付けており、これによりビューファイルなどでURLを直接書く代わりに route('contact.index') のように呼び出せるため、後からURLを変更する際に便利です。

変更点: ナビゲーションバーにお問い合わせページへのリンクを追加

/resources/views/layout.blade.php
1+                    <li class="nav-item">
2+                        <a class="nav-link text-white" href="{{ route('contact.index') }}">お問い合わせ</a>
3+                    </li>

全てのページで共通して使われるレイアウトファイル layout.blade.php のナビゲーションバーに、お問い合わせフォームへのリンクを追加しました。href属性には、先ほどroutes/web.phpで定義した名前付きルート contact.indexroute() ヘルパー関数を使って指定しています。これにより、どのページからでもお問い合わせページへ移動できるようになります。

変更点: お問い合わせフォームの処理を担うコントローラーを作成

/app/Http/Controllers/ContactController.php
1+<?php
2+
3+namespace App\Http\Controllers;
4+
5+use Illuminate\Http\Request;
6+use Illuminate\Support\Facades\Mail;
7+use App\Mail\ContactMail;
8+
9+class ContactController extends Controller
10+{
11+    // ...メソッドの定義...
12+}

お問い合わせフォームに関する一連の処理をまとめるための ContactController を新しく作成しました。ファイルの先頭では、use文を使って、これから使用するクラス(Request, Mail, ContactMail)を読み込んでいます。これにより、クラス名を省略して記述できるようになります。

変更点: お問い合わせ入力フォームを表示する

/app/Http/Controllers/ContactController.php
1+    /**
2+     * お問い合わせフォームの表示
3+     */
4+    public function index()
5+    {
6+        return view('contact.index');
7+    }

ContactController 内に index メソッドを追加しました。このメソッドは、GET /contact というURLへのアクセスに対応します。処理内容は非常にシンプルで、view() ヘルパー関数を使って resources/views/contact/index.blade.php というビューファイルを表示するよう指示しています。これが、ユーザーが最初に見るお問い合わせの入力画面となります。

変更点: お問い合わせ内容の入力フォームを作成

/resources/views/contact/index.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h2>お問い合わせ</h2>
5+
6+    <form action="{{ route('contact.confirm') }}" method="POST">
7+        @csrf
8+        <div class="mb-3">
9+            <label for="name" class="form-label">お名前</label>
10+            <input type="text" name="name" id="name" class="form-control" value="{{ old('name') }}" required>
11+        </div>
12+        <div class="mb-3">
13+            <label for="email" class="form-label">メールアドレス</label>
14+            <input type="email" name="email" id="email" class="form-control" value="{{ old('email') }}" required>
15+        </div>
16+        <div class="mb-3">
17+            <label for="message" class="form-label">メッセージ</label>
18+            <textarea name="message" id="message" class="form-control" rows="5" required>{{ old('message') }}</textarea>
19+        </div>
20+        <button type="submit" class="btn btn-primary">確認</button>
21+    </form>
22+@endsection

お問い合わせ内容を入力するためのHTMLフォーム contact/index.blade.php を作成しました。

  • <form> タグの action 属性には、データの送信先として contact.confirm ルートを指定しています。method="POST" は、データを送信する方法を指定するものです。
  • @csrf は、Laravelが提供するセキュリティ機能(CSRF対策)です。フォームを安全に送信するために必須の記述です。
  • 各入力項目には name 属性(name, email, message)が設定されており、これがコントローラーでデータを受け取る際のキーになります。
  • value="{{ old('name') }}" は、バリデーションエラーなどで入力画面に戻された際に、直前に入力した値をフォームに再表示するための記述です。

変更点: 入力内容を検証し、確認画面へ渡す

/app/Http/Controllers/ContactController.php
1+    /**
2+     * 確認画面の表示
3+     */
4+    public function confirm(Request $request)
5+    {
6+        $request->validate([
7+            'name' => 'required|string|max:255',
8+            'email' => 'required|email|max:255',
9+            'message' => 'required|string|max:1000',
10+        ]);
11+
12+        // 確認画面を表示
13+        $contactData = $request->all();
14+        return view('contact.confirm', compact('contactData'));
15+    }

ContactControllerconfirm メソッドを追加しました。入力フォームからPOSTされたデータは、引数の Request $request で受け取ります。

  • $request->validate() は、入力値の検証(バリデーション)を行うメソッドです。各項目に対してルール(例: requiredは必須、emailはメール形式)を指定し、ルールに違反した場合は自動的に入力画面にエラーメッセージ付きで戻されます。
  • バリデーションを通過した場合、$request->all() で全ての入力データを配列として取得します。
  • 最後に view('contact.confirm', compact('contactData')) で、取得したデータを contactData という変数名で確認画面のビュー (contact.confirm) に渡して表示します。

変更点: 入力内容の確認画面を作成

/resources/views/contact/confirm.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h2>お問い合わせ確認</h2>
5+
6+    <form action="{{ route('contact.send') }}" method="POST">
7+        @csrf
8+        <input type="hidden" name="name" value="{{ $contactData['name'] }}">
9+        <input type="hidden" name="email" value="{{ $contactData['email'] }}">
10+        <input type="hidden" name="message" value="{{ $contactData['message'] }}">
11+
12+        <div class="mb-3">
13+            <label class="form-label">お名前</label>
14+            <p>{{ $contactData['name'] }}</p>
15+        </div>
16+        {{-- ...メールアドレスとメッセージの表示... --}}
17+
18+        <button type="button" onclick="history.back()" class="btn btn-secondary">修正</button>
19+        <button type="submit" class="btn btn-primary">送信</button>
20+    </form>
21+@endsection

入力内容の確認画面 contact/confirm.blade.php を作成しました。

  • コントローラーから渡された $contactData を使い、入力された名前、メールアドレス、メッセージを表示します。
  • form の送信先は、メール送信処理を行う contact.send ルートに設定されています。
  • 表示されているデータを次の処理に引き継ぐため、<input type="hidden"> を使って、画面には見えない形でフォームにデータを含めています。
  • 「修正」ボタンには onclick="history.back()" というJavaScriptを記述し、ブラウザの「戻る」機能で入力画面に戻れるようにしています。

変更点: 送信するメールの内容を定義するMailableクラスを作成

/app/Mail/ContactMail.php
1+<?php
2+
3+namespace App\Mail;
4+
5+use Illuminate\Bus\Queueable;
6+use Illuminate\Contracts\Queue\ShouldQueue;
7+use Illuminate\Mail\Mailable;
8+use Illuminate\Mail\Mailables\Content;
9+use Illuminate\Mail\Mailables\Envelope;
10+use Illuminate\Queue\SerializesModels;
11+
12+class ContactMail extends Mailable
13+{
14+    use Queueable, SerializesModels;
15+
16+    public $contact;
17+    public $role;
18+
19+    public function __construct($contact, $role)
20+    {
21+        $this->contact = $contact;
22+        $this->role = $role;
23+    }
24+
25+    public function envelope(): Envelope
26+    {
27+        return new Envelope(
28+            subject: $this->role === 'admin' ? '新しいお問い合わせがありました' : 'お問い合わせありがとうございます',
29+        );
30+    }
31+
32+    public function content(): Content
33+    {
34+        return new Content(
35+            view: 'emails.contact',
36+            with: [
37+                'contact' => $this->contact,
38+                'role' => $this->role
39+            ],
40+        );
41+    }
42+}

送信するメールの構造を定義するための ContactMail クラスを新しく作成しました。これはLaravelのMailableクラスを継承しており、メールに関する設定をカプセル化(一つのクラスにまとめること)できます。

  • __construct メソッド(コンストラクタ)は、このクラスが使われる際に、コントローラーからお問い合わせ内容 ($contact) と送信先の役割 ($role) を受け取ります。
  • envelope メソッドでは、メールの件名(subject)を設定します。$this->role'admin' かどうかで件名を動的に切り替えています。
  • content メソッドでは、メール本文に使用するビューファイル (emails.contact) と、そのビューに渡すデータ($contact$role)を指定しています。

変更点: メール本文のテンプレートを作成

/resources/views/emails/contact.blade.php
1+<!DOCTYPE html>
2+<html lang="ja">
3+<body>
4+    <h1>{{ $role === 'admin' ? '新しいお問い合わせ' : 'お問い合わせ内容' }}</h1>
5+
6+    <p><strong>お名前:</strong> {{ $contact['name'] }}</p>
7+    <p><strong>メールアドレス:</strong> {{ $contact['email'] }}</p>
8+    <p><strong>メッセージ:</strong></p>
9+    <p>{{ $contact['message'] }}</p>
10+</body>
11+</html>

メールの本文となるHTMLファイル emails/contact.blade.php を作成しました。これは ContactMail クラスから呼び出されます。Mailableクラスから渡された変数 $role$contact を使って、管理者向けとユーザー向けでタイトルを切り替えたり、お問い合わせ内容を表示したりしています。通常のBladeビューと同じ構文が使用できます。

変更点: メールを送信し、完了画面へリダイレクトする

/app/Http/Controllers/ContactController.php
1+    /**
2+     * お問い合わせ内容の送信処理
3+     */
4+    public function send(Request $request)
5+    {
6+        $request->validate([
7+            'name' => 'required|string|max:255',
8+            'email' => 'required|email|max:255',
9+            'message' => 'required|string|max:1000',
10+        ]);
11+
12+        $contactData = $request->only('name', 'email', 'message');
13+
14+        // メールの送信(運営者)
15+        Mail::to(config('mail.from.address'))->send(new ContactMail($contactData, 'admin'));
16+
17+        // メールの送信(ユーザー)
18+        Mail::to($request->email)->send(new ContactMail($contactData, 'user'));
19+
20+        return redirect()->route('contact.complete');
21+    }

ContactControllersend メソッドを追加しました。確認画面から送信されたデータを使って、実際にメールを送信する処理です。

  • まず、不正なアクセスを防ぐため、confirm メソッドと同様に再度バリデーションを実行しています。
  • Mail::to()->send() を使ってメールを送信します。
  • 1通目は管理者宛です。送信先は config('mail.from.address') で設定ファイルから取得し、ContactMail クラスには役割として 'admin' を渡しています。
  • 2通目はユーザー宛の自動返信メールです。送信先はフォームに入力されたメールアドレス ($request->email) を指定し、役割として 'user' を渡しています。
  • 全てのメール送信処理が終わったら、redirect()->route('contact.complete') で完了画面へリダイレクト(ページ遷移)させます。

変更点: 送信完了画面を表示する

/app/Http/Controllers/ContactController.php
1+    public function complete()
2+    {
3+        return view('contact.complete');
4+    }

ContactControllercomplete メソッドを追加しました。send メソッドからリダイレクトされた後、このメソッドが呼び出されます。処理内容は index メソッドと同様にシンプルで、resources/views/contact/complete.blade.php ファイルをブラウザに表示するだけです。

変更点: お問い合わせ完了メッセージを表示する画面を作成

/resources/views/contact/complete.blade.php
1+@extends('layout')
2+
3+@section('content')
4+    <h1>お問い合わせが完了しました</h1>
5+    <p>メールが正常に送信されました。ご連絡ありがとうございます。</p>
6+    <p>今後ともよろしくお願いいたします。</p>
7+    <a href="{{ route('posts.index') }}">掲示板一覧に戻る</a>
8+@endsection

お問い合わせが完了したことをユーザーに伝えるための contact/complete.blade.php を作成しました。このページは、完了メッセージとトップページ(掲示板一覧)へ戻るためのリンクを表示する静的な内容となっています。

おわりに

この記事では、お問い合わせフォームの入力からメール送信、完了画面までの一連の流れを実装しました。ContactControllerでバリデーションによって入力値をチェックし、入力・確認・完了と画面を遷移させる方法を学びました。メール送信ではMailableクラスを利用し、管理者とユーザーで件名や本文を切り替えて自動返信する仕組みを作りました。ここで学んだフォーム処理は、様々なWebアプリケーションで応用できる重要な基本機能です。

関連コンテンツ

関連IT用語