PHP | 問い合わせフォームの完全版コード – MVCアーキテクチャで構築

はじめに

本記事では、PHPとVue.jsを活用して構築した問い合わせフォームのシステムにおけるMVCアーキテクチャの完全版コードを紹介します。前回の記事では、MVCパターンへのリファクタリングについて解説しましたが、今回は掲載済みのコードに加えてその他のファイルもすべて紹介します。

これまでの詳細な説明は以下の記事を参照してください:

  1. これまで作ってきたフォーム
  2. リファクタリング – MVCパターンで保守性と拡張性を向上

ファイル構成

以下が最終的なプロジェクト構成です。

api/
├── config/
│   └── config.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/
├── .htaccess
├── routes.php
.htaccess
index.html

各ファイルのコード

config/config.php

<?php
return [
    // reCAPTCHA 設定
    'recaptcha' => [
        'site_key' => 'your_site_key', // サイトキー
        'secret_key' => 'your_secret_key', // シークレットキー
    ],

    // メール設定
    'email' => [
        'admin' => 'admin@example.com', // 管理者メールアドレス
        'from' => 'admin@example.com', // 差出人メールアドレス
    ],

    // CSRF 設定
    'csrf' => [
        'token_expiry' => 3600, // トークン有効期限(秒単位)
    ],

    // データベース接続設定
    'database' => [
        'host' => 'localhost', // データベースホスト
        'username' => 'db_user', // ユーザー名
        'password' => 'db_password', // パスワード
        'name' => 'db_name', // データベース名
        'charset' => 'utf8mb4', // 文字コード
    ],

    // デバッグモード
    'debug' => true, // デバッグモード(true:有効, false:無効)

    // タイムゾーン設定
    'timezone' => 'Asia/Tokyo', // タイムゾーン

    // エラーログ設定
    'log' => [
        'error_log' => __DIR__ . '/../logs/error.log', // エラーログの保存先
    ],

    // CORS設定
    'cors' => [
        'allowed_origins' => '*', // 許可するオリジン(* はすべて許可)
        'allowed_methods' => ['GET', 'POST', 'OPTIONS'], // 許可するHTTPメソッド
        'allowed_headers' => ['Content-Type', 'X-CSRF-TOKEN'], // 許可するヘッダー
    ],
];

controllers/AppController.php

<?php
class AppController {
    /**
     * @var array 設定データ
     */
    protected $config;

    /**
     * コンストラクタ
     * 設定の読み込みと初期ヘッダー設定を実施
     */
    public function __construct() {
        // 設定ファイルの読み込み
        if (empty($this->config)) {
            $this->config = require __DIR__ . '/../config/config.php';
        }

        $this->setCorsHeaders(); // CORS設定適用
        $this->setSecurityHeaders(); // セキュリティヘッダー適用
        $this->handlePreflight(); // OPTIONSリクエスト対応
    }

    /**
     * 設定値取得メソッド
     * - ドット記法でネストした設定値を取得可能
     *
     * @param string $key 設定キー(例: 'recaptcha.site_key')
     * @param mixed $default デフォルト値(存在しない場合の返却値)
     * @return mixed 設定値またはデフォルト値
     */
    protected function getConfig($key, $default = null) {
        $keys = explode('.', $key); // ドット記法に対応
        $value = $this->config;

        foreach ($keys as $k) {
            if (isset($value[$k])) {
                $value = $value[$k];
            } else {
                return $default;
            }
        }
        return $value;
    }

    /**
     * JSONレスポンス送信メソッド
     * - APIからJSON形式でレスポンスを返す
     *
     * @param array $data レスポンスデータ
     * @param int $statusCode HTTPステータスコード(デフォルト: 200)
     */
    protected function sendJsonResponse($data, $statusCode = 200) {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }

    /**
     * エラーレスポンス送信メソッド
     * - エラーメッセージとステータスコードを返す
     * - デバッグモード時には追加情報を含める
     *
     * @param string $message エラーメッセージ
     * @param int $statusCode HTTPステータスコード(デフォルト: 400)
     * @param array $debugData デバッグ用追加情報
     */
    protected function sendErrorResponse($message, $statusCode = 400, $debugData = []) {
        $this->logError($message); // ログにエラーメッセージを記録

        $response = ['success' => false, 'error' => $message];
        if ($this->getConfig('debug', false) && !empty($debugData)) {
            $response['debug'] = $debugData; // デバッグ情報追加
        }

        $this->sendJsonResponse(['success' => false, 'error' => $message], $statusCode);
    }

    /**
     * 成功レスポンス送信メソッド
     * - 成功フラグとデータを返す
     *
     * @param array $data レスポンスデータ
     */
    protected function sendSuccessResponse($data = []) {
        $this->sendJsonResponse(array_merge(['success' => true], $data));
    }

    /**
     * CORSヘッダー設定
     * - クロスオリジンリクエストを許可するヘッダーを設定
     */
    protected function setCorsHeaders() {
        header('Access-Control-Allow-Origin: ' . $this->getConfig('cors.allowed_origins', '*'));
        header('Access-Control-Allow-Methods: ' . implode(',', $this->getConfig('cors.allowed_methods', ['GET', 'POST', 'OPTIONS'])));
        header('Access-Control-Allow-Headers: ' . implode(',', $this->getConfig('cors.allowed_headers', ['Content-Type', 'X-CSRF-TOKEN'])));
    }

    /**
     * セキュリティヘッダー設定
     * - 基本的なセキュリティ対策ヘッダーを追加
     */
    protected function setSecurityHeaders() {
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('X-XSS-Protection: 1; mode=block');
    }

    /**
     * HTTPメソッド制限
     * - 許可されたHTTPメソッドのみを受け入れる
     * - 不正なリクエストはエラーレスポンスで終了
     *
     * @param array $allowedMethods 許可するHTTPメソッド
     */
    public function validateHttpMethod($allowedMethods) {
        error_log('----------');
        error_log('Method: ' . $_SERVER['REQUEST_METHOD']);
        error_log('Allowed: ' . implode(', ', $allowedMethods));
        error_log('Request URI: ' . $_SERVER['REQUEST_URI']);
        error_log('Headers: ' . print_r(getallheaders(), true));
        $input = file_get_contents('php://input');
        error_log('Input: ' . $input);

        if (!in_array(strtoupper($_SERVER['REQUEST_METHOD']), $allowedMethods)) {
            error_log('拒否されたリクエスト: ' . $_SERVER['REQUEST_METHOD']);
            $this->sendErrorResponse('許可されていないHTTPメソッドです', 405);
            exit;
        }
    }

    /**
     * プリフライトリクエスト対応
     * - OPTIONSメソッドに204ステータスで応答
     */
    public function handlePreflight() {
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(204); // No Content
            exit;
        }
    }

    /**
     * エラーログ記録
     * - エラーメッセージをログファイルに記録
     *
     * @param string $message エラーメッセージ
     */
    protected function logError($message) {
        $logPath = $this->getConfig('log.error_log', __DIR__ . '/../logs/error.log');
        error_log('[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL, 3, $logPath);
    }

    /**
     * デバッグログ記録
     * - デバッグモード時のみログ出力
     *
     * @param string $message デバッグメッセージ
     */
    protected function debugLog($message) {
        if ($this->getConfig('debug', false)) {
            error_log('[DEBUG][' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL);
        }
    }
}

controllers/ContactController.php

<?php
require_once 'AppController.php';
require_once __DIR__ . '/../models/ContactModel.php';
require_once __DIR__ . '/../services/EmailService.php';
require_once __DIR__ . '/../services/CSRFService.php';
require_once __DIR__ . '/../services/RecaptchaService.php';
require_once __DIR__ . '/../services/ValidationService.php';
require_once __DIR__ . '/../services/CsvService.php';
require_once __DIR__ . '/../services/DatabaseService.php';

/**
 * 問い合わせフォーム コントローラ
 * - 入力検証、CSRF対策、reCAPTCHA検証、データ保存、メール通知を管理
 */
class ContactController extends AppController {
    /** @var ContactModel モデルインスタンス */
    protected $contactModel;
    /** @var EmailService メール送信サービス */
    protected $emailService;
    /** @var CSRFService CSRF対策サービス */
    protected $csrfService;
    /** @var RecaptchaService reCAPTCHA検証サービス */
    protected $recaptchaService;
    /** @var ValidationService 入力検証サービス */
    protected $validationService;
    /** @var CsvService CSV保存サービス */
    protected $csvService;
    /** @var DatabaseService データベース操作サービス */
    protected $databaseService;

    /**
     * コンストラクタ
     * - サービスやモデルの初期化
     * - CORSとHTTPメソッド制限の設定
     */
    public function __construct() {
        parent::__construct(); // 親クラスの設定を適用

        // OPTIONSリクエストを許可 (CORS対応)
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            header('Access-Control-Allow-Origin: *');
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
            header('Access-Control-Allow-Headers: Content-Type, X-CSRF-TOKEN, Authorization');
            http_response_code(204); // No Content
            exit;
        }

        // HTTPメソッド検証 (POSTのみ許可)
        if ($_SERVER['REQUEST_URI'] === '/api/contact') {
            $this->validateHttpMethod(['POST']);
        }

        // サービスとモデルの初期化
        $this->contactModel = new ContactModel($this->config);
        $this->emailService = new EmailService($this->config);
        $this->csrfService = new CSRFService();
        $this->recaptchaService = new RecaptchaService($this->config);
        $this->validationService = new ValidationService();
        $this->csvService = new CsvService();
        $this->databaseService = new DatabaseService($this->config);
    }

    /**
     * 問い合わせフォームのリクエスト処理
     * - 入力データの検証、保存、メール送信
     */
    public function handleRequest() {
        try {
            // 入力データの取得
            $data = json_decode(file_get_contents('php://input'), true);

            // CSRFトークン検証
            if (!$this->csrfService->validateCsrfToken($_SERVER['HTTP_X_CSRF_TOKEN'])) {
                $this->sendErrorResponse('CSRFトークンが無効です。');
            }

            // reCAPTCHA検証
            if (!$this->recaptchaService->validate($data['recaptcha_token'])) {
                $this->sendErrorResponse('reCAPTCHA検証に失敗しました。');
            }

            // 入力データの検証とサニタイズ
            $name = $this->validationService->sanitizeInput($data['name']);
            $email = $this->validationService->validateEmail($data['email']);
            $message = $this->validationService->sanitizeInput($data['message']);

            if (!$email) {
                $this->sendErrorResponse('メールアドレスが無効です。');
            }

            // ユーザー情報取得
            $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '不明';
            $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '不明';

            // CSV保存
            $csvPath = __DIR__ . '/../storage/submissions.csv';
            $timestamp = date('Y-m-d H:i:s');
            $csvData = [$timestamp, $name, $email, $message, $ipAddress, $userAgent];
            if (!$this->csvService->saveToCsv($csvPath, $csvData)) {
                $this->sendErrorResponse('CSVファイルへの保存に失敗しました。');
            }

            // データベース保存
            $sql = "INSERT INTO submissions (name, email, message, ip_address, user_agent) VALUES (?, ?, ?, ?, ?)";
            $params = [$name, $email, $message, $ipAddress, $userAgent];
            if (!$this->databaseService->insert($sql, $params)) {
                $this->sendErrorResponse('データベース保存に失敗しました。');
            }

            // メール送信
            $adminMailSent = $this->emailService->sendAdminNotification($name, $email, $message);
            $userMailSent = $this->emailService->sendUserConfirmation($name, $email, $message);

            if ($adminMailSent && $userMailSent) {
                $this->sendSuccessResponse(['message' => 'お問い合わせを受け付けました。']);
            } else {
                $this->sendErrorResponse('メール送信に失敗しました。');
            }
        } catch (Exception $e) {
            $this->sendErrorResponse('サーバーエラーが発生しました: ' . $e->getMessage(), 500);
        }
    }
}

controllers/CsrfController.php

<?php
require_once 'AppController.php';

/**
 * CSRFトークン管理クラス
 * - CSRFトークンの発行と取得を処理
 */
class CsrfController extends AppController
{

    /**
     * コンストラクタ
     * - HTTPメソッドの制限を適用(GETおよびOPTIONSを許可)
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * CSRFトークンの取得メソッド
     * - セッションを開始し、トークンが存在しない場合は新規生成
     * - 生成されたトークンをJSONレスポンスで返却
     *
     * @return void JSON形式でCSRFトークンを返却
     */
    public function getToken()
    {

        // HTTPメソッド制限をGETおよびOPTIONSに対応
        $this->validateHttpMethod(['GET', 'OPTIONS']);

        // セッションが開始されていない場合は開始
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        // トークン生成
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 32バイトのランダムデータを生成
        }

        // 成功レスポンスでトークンを返却
        $this->sendSuccessResponse(['csrf_token' => $_SESSION['csrf_token']]);
    }
}

controllers/RecaptchaController.php

<?php
require_once 'AppController.php';

/**
 * reCAPTCHA管理クラス
 * - サイトキーの取得とトークン検証の機能を提供
 */
class RecaptchaController extends AppController {

    /**
     * コンストラクタ
     * - 親クラスの初期設定を適用
     */
    public function __construct() {
        parent::__construct(); // 親クラスの設定を適用
    }

    /**
     * reCAPTCHAサイトキーを返すメソッド
     * - フロントエンドでreCAPTCHAを利用するためのキーを提供
     * - 許可されたHTTPメソッドはGETのみ
     *
     * @return void JSON形式でサイトキーを返却
     */
    public function getSiteKey() {

        // HTTPメソッド制限(GETのみ許可)
        $this->validateHttpMethod(['GET']);

        // 設定からreCAPTCHAサイトキーを取得
        $siteKey = $this->getConfig('recaptcha.site_key');
        if (!$siteKey) {
            $this->sendErrorResponse('reCAPTCHAキーが設定されていません。');
        }

        // 成功レスポンスでキーを返却
        $this->sendSuccessResponse(['site_key' => $siteKey]);
    }

    /**
     * reCAPTCHAトークン検証メソッド
     * - トークンの有効性をGoogleのAPIで確認
     * - 許可されたHTTPメソッドはPOSTのみ
     *
     * @param string $token フロントエンドから提供されたreCAPTCHAトークン
     * @return bool 検証成功時はtrue、それ以外はエラーレスポンスを返却
     */
    public function validateRecaptcha($token) {
        // HTTPメソッド制限(POSTのみ許可)
        $this->validateHttpMethod(['POST']);

        // シークレットキー取得
        $secretKey = $this->getConfig('recaptcha.secret_key');

        // Google API経由で検証
        $response = @file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=$secretKey&response=$token");

        // 通信エラーチェック
        if (!$response) {
            $this->sendErrorResponse('reCAPTCHAサーバーとの通信に失敗しました。');
        }

        // 結果解析
        $result = json_decode($response, true);
        if (!$result || !$result['success'] || $result['score'] < 0.5) {
            $this->sendErrorResponse('reCAPTCHA検証に失敗しました。信頼スコアが低すぎます。');
        }

        // 検証成功
        return true;
    }
}

models/ContactModel.php

<?php
require_once __DIR__ . '/../services/DatabaseService.php';

/**
 * 問い合わせデータモデルクラス
 * - データベースへの問い合わせ情報の保存機能を提供
 */
class ContactModel {
    /** @var DatabaseService データベース操作サービス */
    protected $databaseService;

    /**
     * コンストラクタ
     * - データベースサービスを初期化
     *
     * @param array $config データベース設定情報
     */
    public function __construct($config) {
        // データベースサービスを利用
        $this->databaseService = new DatabaseService($config);
    }

    /**
     * 問い合わせデータの保存
     * - 問い合わせ情報をデータベースに挿入
     *
     * @param array $data 保存するデータ (name, email, message, ip_address, user_agent)
     * @return bool 成功時はtrue、失敗時はfalseを返却
     */
    public function saveContact($data) {
        try {
            // データ挿入SQL
            $sql = "INSERT INTO contacts (name, email, message, ip_address, user_agent) 
                    VALUES (?, ?, ?, ?, ?)";
            $params = [
                $data['name'],         // 名前
                $data['email'],        // メールアドレス
                $data['message'],      // メッセージ
                $data['ip_address'],   // IPアドレス
                $data['user_agent']    // ユーザーエージェント
            ];

            // データベースサービスを使用して挿入
            return $this->databaseService->insert($sql, $params);
        } catch (Exception $e) {
            // エラーログ記録
            error_log('ContactModel Error: ' . $e->getMessage());
            return false;
        }
    }
}

services/CSRFService.php

<?php
class CSRFService {

    /**
     * CSRFトークン検証
     * - セッション内のトークンとリクエストトークンを比較して一致を確認
     *
     * @param string $token クライアントから送信されたCSRFトークン
     * @return bool トークンが一致すればtrue、不一致ならfalseを返却
     */
    public function validateCsrfToken($token) {
        // セッションが開始されていない場合は開始
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        // デバッグ用ログ出力
        error_log('セッションID: ' . session_id());
        error_log('セッション内トークン: ' . ($_SESSION['csrf_token'] ?? '未設定'));
        error_log('リクエストトークン: ' . $token);

        // トークン比較
        if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
            error_log('CSRFトークンが不一致です');
            error_log('セッションID: ' . session_id());
            error_log('セッション内トークン: ' . ($_SESSION['csrf_token'] ?? '未設定'));
            error_log('リクエストトークン: ' . $token);
            return false;
        }
        return true;
    }

    /**
     * CSRFトークン生成
     * - セッション内にトークンが存在しない場合、新規に生成
     *
     * @return string 生成されたCSRFトークン
     */
    public function generateCsrfToken() {
        // セッションが開始されていない場合は開始
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        // トークン生成
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 32バイトのランダムデータを生成
        }
        return $_SESSION['csrf_token'];
    }
}

services/CsvService.php

<?php
class CsvService {

    /**
     * CSVファイルへのデータ保存
     * - 指定されたパスのCSVファイルにデータを追記
     *
     * @param string $filePath 保存先のCSVファイルパス
     * @param array $data 保存するデータ (配列形式)
     * @return bool 成功時はtrue、失敗時はfalseを返却
     */
    public function saveToCsv($filePath, $data) {
        try {
            // CSVファイルを追記モードでオープン
            $fileHandle = fopen($filePath, 'a');
            if (!$fileHandle) {
                throw new Exception('CSVファイルのオープンに失敗しました。');
            }

            // データをCSV形式で書き込み
            if (fputcsv($fileHandle, $data) === false) {
                throw new Exception('CSVファイルへの書き込みに失敗しました。');
            }

            // ファイルをクローズ
            fclose($fileHandle);
            return true;
        } catch (Exception $e) {
            // エラーログ記録
            error_log('CSV保存エラー: ' . $e->getMessage());
            return false;
        }
    }
}

services/DatabaseService.php

<?php
class DatabaseService {
    /** @var PDO PDOインスタンス */
    protected $pdo;

    /**
     * コンストラクタ
     * - データベース接続を初期化
     *
     * @param array $config データベース設定情報
     */
    public function __construct($config) {
        // DSN(データソース名)の設定
        $dsn = 'mysql:host=' . $config['database']['host'] . ';dbname=' . $config['database']['name'] . ';charset=utf8';

        // PDOインスタンスの生成とエラーモード設定
        $this->pdo = new PDO($dsn, $config['database']['username'], $config['database']['password']);
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    /**
     * データ挿入メソッド
     * - SQLのINSERT文を実行
     *
     * @param string $sql 実行するSQLクエリ
     * @param array $params バインドするパラメータ
     * @return bool 実行結果(成功時はtrue、失敗時はfalse)
     */
    public function insert($sql, $params) {
        // SQLクエリの準備と実行
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute($params);
    }
}

services/EmailService.php

<?php
class EmailService {
    /** @var array 設定データ */
    protected $config;

    /**
     * コンストラクタ
     * - メール設定情報を初期化
     *
     * @param array $config メール設定情報
     */
    public function __construct($config) {
        $this->config = $config;
    }

    /**
     * メール送信メソッド
     * - 指定された宛先にメールを送信
     *
     * @param string $to 宛先メールアドレス
     * @param string $subject メール件名
     * @param string $message メール本文
     * @return bool メール送信結果(成功時はtrue、失敗時はfalse)
     */
    public function sendMail($to, $subject, $message) {
        $headers = 'From: ' . $this->config['email']['from'];
        return mail($to, $subject, $message, $headers);
    }

    /**
     * 管理者への通知メール送信
     * - 問い合わせ内容を管理者に通知
     *
     * @param string $name 送信者名
     * @param string $email 送信者メールアドレス
     * @param string $message 問い合わせ内容
     * @return bool メール送信結果(成功時はtrue、失敗時はfalse)
     */
    public function sendAdminNotification($name, $email, $message) {
        $adminEmail = $this->config['email']['admin'];
        $subject = 'お問い合わせフォームからのメッセージ';
        $body = "名前: $name\nメールアドレス: $email\nメッセージ: $message";
        return $this->sendMail($adminEmail, $subject, $body);
    }

    /**
     * ユーザーへの確認メール送信
     * - 問い合わせ受領確認をユーザーに送信
     *
     * @param string $name 送信者名
     * @param string $email 送信者メールアドレス
     * @param string $message 問い合わせ内容
     * @return bool メール送信結果(成功時はtrue、失敗時はfalse)
     */
    public function sendUserConfirmation($name, $email, $message) {
        $subject = 'お問い合わせありがとうございます';
        $body = "$name 様\n\nお問い合わせありがとうございます。\n以下の内容で受け付けました。\n\n----------------------\nお名前: $name\nメールアドレス: $email\nお問い合わせ内容:\n$message\n----------------------\n\n担当者より折り返しご連絡いたします。\n\nよろしくお願いいたします。\n";
        return $this->sendMail($email, $subject, $body);
    }
}

services/RecaptchaService.php

<?php
class RecaptchaService {
    /** @var array 設定データ */
    protected $config;

    /**
     * コンストラクタ
     * - reCAPTCHA設定情報を初期化
     *
     * @param array $config reCAPTCHA設定情報
     */
    public function __construct($config) {
        $this->config = $config;
    }

    /**
     * reCAPTCHAトークン検証
     * - GoogleのAPIを利用してreCAPTCHAトークンの有効性を確認
     *
     * @param string $token クライアントから送信されたreCAPTCHAトークン
     * @return bool トークン検証成功時はtrue、失敗時はfalseを返却
     */
    public function validate($token) {
        // GoogleのreCAPTCHA検証エンドポイント
        $url = 'https://www.google.com/recaptcha/api/siteverify';

        // APIリクエストを送信して結果を取得
        $response = file_get_contents($url . '?secret=' . $this->config['recaptcha']['secret_key'] . '&response=' . $token);
        $result = json_decode($response, true);

        // 検証結果を返却
        return $result['success'] ?? false;
    }
}

services/ValidationService.php

<?php
class ValidationService {

    /**
     * 入力値のサニタイズ
     * - HTMLタグ除去とエンティティ変換を行い、安全な文字列に変換
     *
     * @param string $input サニタイズ対象の入力値
     * @return string サニタイズされた安全な文字列
     */
    public function sanitizeInput($input) {
        return htmlspecialchars(strip_tags($input), ENT_QUOTES, 'UTF-8');
    }

    /**
     * メールアドレスの検証
     * - 入力値が有効なメールアドレス形式かどうかを確認
     *
     * @param string $email 検証対象のメールアドレス
     * @return bool|string 有効な場合はメールアドレス、無効な場合はfalseを返却
     */
    public function validateEmail($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }

    /**
     * 必須フィールドの検証
     * - 入力値が空でないかを確認
     *
     * @param mixed $value 検証対象の値
     * @return bool 空でない場合はtrue、空の場合はfalseを返却
     */
    public function validateRequired($value) {
        return !empty($value);
    }
}

routes.php

<?php
// コントローラーの読み込み
require_once __DIR__ . '/controllers/ContactController.php';
require_once __DIR__ . '/controllers/CsrfController.php';
require_once __DIR__ . '/controllers/RecaptchaController.php';
require_once __DIR__ . '/controllers/ErrorController.php';

/**
 * OPTIONSリクエストへの対応
 * - CORSポリシー設定と事前検証リクエストに対応
 */
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header('Access-Control-Allow-Origin: *');
    header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
    header('Access-Control-Allow-Headers: Content-Type, X-CSRF-TOKEN, Authorization');
    http_response_code(204); // No Content
    exit;
}

// HTTPメソッドとURIの取得
$method = $_SERVER['REQUEST_METHOD'];
$uri = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');

// コントローラーのインスタンス化
$contactController = new ContactController();
$csrfController = new CsrfController();
$recaptchaController = new RecaptchaController();
$errorController = new ErrorController();

/**
 * ルーティング処理
 * - HTTPメソッドとURIの組み合わせによって適切な処理を呼び出す
 */
switch (true) {
    // お問い合わせフォーム処理
    case $method === 'POST' && $uri === 'api/contact':
        $contactController->handleRequest();
        break;

    // CSRFトークン取得
    case $method === 'GET' && $uri === 'api/csrf-token':
        $csrfController->getToken();
        break;

    // reCAPTCHAキー取得
    case $method === 'GET' && $uri === 'api/recaptcha-key':
        $recaptchaController->getSiteKey();
        break;

    // reCAPTCHA検証
    case $method === 'POST' && $uri === 'api/recaptcha-validate':
        $recaptchaController->validateRecaptcha($_POST['token']);
        break;

    // 404エラーハンドリング
    default:
        $errorController->notFound();
        break;
}

まとめ

本記事では、問い合わせフォームシステムの完全版コードを掲載しました。すべてのファイルを網羅することで、構築やカスタマイズに役立てることができます。今後は、テストコードやデプロイ手順についての記事も予定していますので、ぜひご期待ください!

コメント

タイトルとURLをコピーしました