1. はじめに
この記事では、Vue.js を使用した問い合わせフォームの機能拡張について解説します。具体的には、以下のポイントを中心に変更と追加を行いました。
- reCAPTCHA利用有無の切り替え機能を追加 – 環境変数からreCAPTCHAを有効・無効に設定できる機能を導入しました。
- 項目追加と柔軟な選択肢管理 – 区分や法人名、問い合わせ内容などの項目を追加し、柔軟に選択肢を管理できる仕組みを構築しました。
- コードのモジュール化 – JavaScriptコードを外部ファイルに分け、管理しやすくしました。
- データベーススキーマの拡張 – 新しい項目に対応するデータベーステーブルの構成を変更しました。
2. プロジェクト構成の確認
プロジェクトは以下の構成で管理されています。
api/
├── config/
│ ├── config.php
│ ├── Env.php
├── controllers/
│ ├── AppController.php
│ ├── ContactController.php
│ ├── CsrfController.php
│ ├── ErrorController.php
│ └── RecaptchaController.php
├── logs/
├── models/
│ └── ContactModel.php
├── services/
│ ├── CSRFService.php
│ ├── CsvService.php
│ ├── DatabaseService.php
│ ├── EmailService.php
│ ├── RecaptchaService.php
│ └── ValidationService.php
├── storage/
├── .env
├── .env.example
├── .htaccess
├── routes.php
js/
│ ├── csrf-recaptcha.js
│ └── form-app.js
.htaccess
index.html
3. フォーム画面のサンプル
以下は問い合わせフォームの実際の画面サンプルです。
4. 環境設定管理とreCAPTCHA利用有無の切り替え機能
環境設定管理 (Env.php)
フォームのセキュリティ設定や動作モードを動的に管理するために、Env.php
を用いて環境変数を扱うようにしました。
class Env {
public static function get($key, $default = null) {
$env = parse_ini_file(__DIR__ . '/../.env');
return $env[$key] ?? $default;
}
}
環境設定例 (.env):
RECAPTCHA_ENABLED=true
RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
利用有無の切り替え機能 (RecaptchaService.php)
$this->enabled = Env::get('RECAPTCHA_ENABLED', false) === 'true';
5. reCAPTCHAとCSRF管理の外部ファイル化 (csrf-recaptcha.js)
コード例 (csrf-recaptcha.js):
const API_URL = "/api";
let recaptchaEnabled = false;
// CSRFトークンの取得
fetch(`${API_URL}/csrf-token`)
.then(response => response.json())
.then(data => localStorage.setItem('csrfToken', data.csrf_token))
.catch(() => console.error('CSRFトークンの取得に失敗しました。'));
// reCAPTCHAの有効状態を確認
fetch(`${API_URL}/recaptcha-enabled`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
recaptchaEnabled = data.enabled;
if (recaptchaEnabled) {
fetch(`${API_URL}/recaptcha-key`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.site_key) {
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=${data.site_key}`;
script.async = true;
script.defer = true;
script.onload = () => {
window.recaptchaSiteKey = data.site_key;
grecaptcha.ready(() => {
window.recaptchaReady = true;
});
};
document.head.appendChild(script);
}
});
}
});
6. 入力フォーム機能強化 (form-app.js)
項目追加と設定の柔軟性を考慮し、JavaScriptファイルを以下のように外部化しました。
const { createRouter, createWebHistory } = VueRouter;
const { createApp } = Vue;
const config = {
types: [
{ value: 'corporation', label: '法人' },
{ value: 'individual', label: '個人' }
],
inquiryTypes: [
{ value: 'support', label: 'サポート' },
{ value: 'sales', label: '営業' },
{ value: 'other', label: 'その他' }
]
};
const InputForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">お問い合わせフォーム</h1>
<form @submit.prevent="goToConfirm">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">区分:</label>
<div class="flex space-x-4">
<label v-for="type in config.types" :key="type.value" class="inline-flex items-center">
<input v-model="formData.type" type="radio" :value="type.value" required class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2">{{ type.label }}</span>
</label>
</div>
</div>
<div v-if="formData.type === 'corporation'" class="mb-4">
<label for="companyName" class="block text-sm font-medium text-gray-700">法人名:</label>
<input v-model="formData.companyName" type="text" id="companyName" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700">名前:</label>
<input v-model="formData.name" type="text" id="name" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div class="mb-4">
<label for="email" class="block text-sm font-medium text-gray-700">メールアドレス:</label>
<input v-model="formData.email" type="email" id="email" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div class="mb-4">
<label for="phone" class="block text-sm font-medium text-gray-700">電話番号:</label>
<input v-model="formData.phone" type="tel" id="phone" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div class="mb-4">
<label for="inquiryType" class="block text-sm font-medium text-gray-700">問い合わせ内容:</label>
<select v-model="formData.inquiryType" id="inquiryType" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
<option v-for="inquiry in config.inquiryTypes" :key="inquiry.value" :value="inquiry.value">{{ inquiry.label }}</option>
</select>
</div>
<div class="mb-4">
<label for="message" class="block text-sm font-medium text-gray-700">お問い合わせ内容:</label>
<textarea v-model="formData.message" id="message" required class="mt-1 block w-full border-gray-300 rounded-md shadow-sm"></textarea>
</div>
<button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded">確認画面へ</button>
</form>
</div>
`,
data() {
return {
formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', phone: '', type: '', companyName: '', inquiryType: '', message: '' },
config
};
},
methods: {
goToConfirm() {
localStorage.setItem('formData', JSON.stringify(this.formData));
this.$router.push({ name: 'confirm' });
},
},
};
const ConfirmForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">内容の確認</h1>
<p><strong>区分:</strong> {{ getLabel('types', formData.type) }}</p>
<p v-if="formData.type === 'corporation'"><strong>法人名:</strong> {{ formData.companyName }}</p>
<p><strong>名前:</strong> {{ formData.name }}</p>
<p><strong>メールアドレス:</strong> {{ formData.email }}</p>
<p><strong>電話番号:</strong> {{ formData.phone }}</p>
<p><strong>問い合わせ内容:</strong> {{ getLabel('inquiryTypes', formData.inquiryType) }}</p>
<p><strong>メッセージ:</strong></p>
<pre>{{ formData.message }}</pre>
<div class="flex space-x-4 mt-4">
<button @click="goBack" class="w-1/2 bg-gray-600 text-white py-2 px-4 rounded">戻る</button>
<button @click="sendForm" class="w-1/2 bg-indigo-600 text-white py-2 px-4 rounded">送信する</button>
</div>
</div>
`,
data() {
return {
formData: JSON.parse(localStorage.getItem('formData')) || { name: '', email: '', phone: '', type: '', companyName: '', inquiryType: '', message: '' },
config
};
},
methods: {
goBack() {
this.$router.push({ name: 'input' });
},
getLabel(key, value) {
const option = this.config[key].find(opt => opt.value === value);
return option ? option.label : value;
},
async sendForm() {
try {
const csrfToken = localStorage.getItem('csrfToken');
let recaptchaToken = null;
// reCAPTCHAトークン生成処理を復元
if (window.recaptchaReady && window.recaptchaSiteKey) {
recaptchaToken = await grecaptcha.execute(window.recaptchaSiteKey, { action: 'submit' });
}
const bodyData = {
...this.formData,
...(recaptchaToken ? { recaptcha_token: recaptchaToken } : {})
};
const response = await fetch(`${API_URL}/contact`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify(bodyData),
});
const data = await response.json();
if (data.success) {
localStorage.removeItem('formData');
this.$router.push({ name: 'complete' });
} else {
alert('送信に失敗しました: ' + data.error);
}
} catch (error) {
alert('エラーが発生しました: ' + error.message);
}
},
},
};
const CompleteForm = {
template: `
<div>
<h1 class="text-2xl font-bold mb-4">送信が完了しました</h1>
<p>お問い合わせいただきありがとうございます。</p>
<button @click="goToInput" class="mt-4 w-full bg-indigo-600 text-white py-2 px-4 rounded">フォームに戻る</button>
</div>
`,
methods: {
goToInput() {
this.$router.push({ name: 'input' });
},
},
};
const routes = [
{ path: '/', name: 'input', component: InputForm },
{ path: '/confirm', name: 'confirm', component: ConfirmForm },
{ path: '/complete', name: 'complete', component: CompleteForm },
];
const router = createRouter({ history: createWebHistory(), routes });
const app = createApp({});
app.use(router);
app.mount('#app');
7. データベーススキーマ変更 (SQL)
SQL例:
ALTER TABLE submissions
ADD COLUMN type VARCHAR(20) NOT NULL AFTER id,
ADD COLUMN company_name VARCHAR(255) NULL AFTER type,
ADD COLUMN phone VARCHAR(20) NOT NULL AFTER email,
ADD COLUMN inquiry_type VARCHAR(50) NOT NULL AFTER phone;
8. まとめ
改善ポイント:
- reCAPTCHAの利用有無を簡単に切り替えられるように改善。
- 入力項目とデータ管理を最適化。
- 管理しやすいコード構成に改善し、拡張性を強化。
コメント