こんにちは、カヨです。
SEとしてお仕事しています
コーディング作業も増えてきたので手順などを記録
自分用ですがぜひよかったら使ってください
今回のお題は、
WEBサイトといえばお問い合わせフォーム。
フロントエンド担当しててもバックエンドのお問い合わせフォームの修正を依頼はされるよね。
✔︎WEBサイトのお問い合わせフォームを作成したい人
✔︎PHPmailerでお問い合わせフォーム作成しようとしてる人
WordPressもPHPでお問い合わせフォームが作成されてるらしいので、実際どんなコードで動いているか知りたい人も見てみてくださいね。
前提
✔︎Gmailのアカウントをもっている
✔︎WEBサイトがすでにある、または作成する予定がある
✔︎PHPmailerで実装
今回は手順というよりすでに実装されていたお問い合わせフォームの編集体験からの備忘録なのでコード内の構造についての記事です!
ファイル構造
✔︎実際のファイル構造の記録
これが基本の構造ではないです。実際に動いている機能の状態を記録しておき、つまずいた時にこの位置なら大丈夫だったというのを確認するためです。
✔︎PHPmailer基本ファイル
・contactフォルダ内
・PHPMailerAutoload.php
✔︎+αの機能
セキュリティー対策、バリデーション、関数の格納ファイルなど送受信のみの動作での記述でもいいのですが最終的に+αの機能を必要になると考えるのでファイルが他の解説の方より多めかもしれません。
コード
PHPmailer関連の詳細コード
理解して解説までおとしこめていないので、細かに備忘録としてコードを記録しておく。これ以外にもコードが必要だったり、PHPmailerのシンプルなコード以外も混ざっているかもしれませんがご了承ください。
form.php
<?php
// クリックジャッキング対策
header('X-FRAME-OPTIONS: SAMEORIGIN');
//セッション管理設定
@session_start();
//マルチバイト関数の言語指定
mb_language('japanese');
require_once($_SERVER['DOCUMENT_ROOT'] . '\..\lib\CsrfValidator.php');//CSRF攻撃対策トークン生成・検証
require_once($_SERVER['DOCUMENT_ROOT'] . '\..\vendor\phpmailer\phpmailer\PHPMailerAutoload.php');//PHPMailerメール機能クラス
require_once($_SERVER['DOCUMENT_ROOT'] . '\..\lib\PageUtil.php');//タグ系の除去と改行の <br> 出力
require_once('./lib.php');//バリデーション、reCAPTCHAの検証
// ビューモデル作成
$viewModel = array(
'name' => array(
'value' => '',
'error' => ''
),
'phone_number' => array(
'value' => '',
'error' => ''
),
'email' => array(
'value' => '',
'error' => ''
),
'message' => array(
'value' => '',
'error' => ''
),
'recaptcha' => array(
'value' => '',
'error' => ''
),
);
// メソッドがPOST以外の場合処理(正常データ検証)
if ($_SERVER['REQUEST_METHOD'] != 'POST' || !filter_input(INPUT_POST, 'from_form')) {
include("./form.html");
exit();
}
// バリデーション
$name = new Name((string) filter_input(INPUT_POST, 'name'));
$phone_number = new PhoneNumber((string) filter_input(INPUT_POST, 'phone_number'));
$email = new Email((string) filter_input(INPUT_POST, 'email'));
$message = new Message((string) filter_input(INPUT_POST, 'message'));
$recaptcha = new Recaptcha((string) filter_input(INPUT_POST, 'g-recaptcha-response'));
$csrf_token = (string) filter_input(INPUT_POST, 'csrf_token');
// ビューモデル(form.htmlの入力エラー時の値代入)
$viewModel['name']['value'] = $name->value;
$viewModel['name']['error'] = $name->error;
$viewModel['phone_number']['value'] = $phone_number->value;
$viewModel['phone_number']['error'] = $phone_number->error;
$viewModel['email']['value'] = $email->value;
$viewModel['email']['error'] = $email->error;
$viewModel['message']['value'] = $message->value;
$viewModel['message']['error'] = $message->error;
$viewModel['recaptcha']['error'] = $recaptcha->error;
// var_dump($viewModel);
// エラーがあれば入力フォームページに戻るー➀
$errorTextLength = array_reduce(
$viewModel,
function ($prev, $current) {
return $prev + mb_strlen($current['error']);
},
0
);
// var_dump($errorTextLength);
// エラーがあれば入力フォームページに戻るー➁
if ($errorTextLength > 0) {
include("./form.html");
exit();
}
// SMTPサーバの設定_タイトルと本文(変数に代入)
$subject = mb_encode_mimeheader("フォームからのお問い合わせ " . date("Y-m-d H:i:s"), "ISO-2022-JP-MS", "auto");
$body = <<< EOD
以下の内容でお問合せがありました。
お名前
{$name->value}
ご連絡先
{$phone_number->value}
メールアドレス
{$email->value}
お問い合わせ内容
{$message->value}
EOD;
// SMTPサーバの設定(変数に代入)※箇所なのでこの下のブロックと同じ$変数名の所で意味確認
$body = mb_convert_encoding($body, "ISO-2022-JP-MS", "auto");
$from = "";
$smtp_user = "";
$smtp_password = "";
$to = "";
$reply = $email->value;
// SMTPサーバの設定
$mailer = new PHPMailer(); // PHPMailerのインスタンス生成
$mailer->IsSMTP(); // SMTP使用指定
$mailer->SMTPDebug = 0; // デバッグレベル
$mailer->SMTPAuth = true; //「SMTP認証を使う」という設定(SMTP認証:メールを送信する際にSMTPサーバーに対してユーザー名とパスワードを提供する認証プロセス)
$mailer->CharSet = 'ISO-2022-JP'; // エンコーディング
$mailer->SMTPSecure = 'tls'; // SMTP接続のセキュリティレベル
$mailer->Host = "smtp.gmail.com"; // SMTPサーバーアドレス(:ポート番号)
$mailer->Port = 587; // ポート番号(gmailのSMTPは587)
$mailer->IsHTML(false); // メール送信の形式
$mailer->Username = $smtp_user; // SMTPサーバーのユーザ名
$mailer->Password = $smtp_password; // SMTPサーバーのパスワード
$mailer->AddReplyTo($reply); // 返信先
$mailer->SetFrom($smtp_user); // 送信者
$mailer->From = $from; // 送信者欄
$mailer->AddAddress($to); // 宛先 受信者
// 送信内容設定
$mailer->Subject = $subject; // '件名';
$mailer->Body = $body; //'メッセージ本文';
// 送信実行とその結果で遷移先を判断
if (!$mailer->Send()) {
$message = "Message was not sent to me<br/ >";
$message .= "Mailer Error: " . $mailer->ErrorInfo;
error_log($message);
// echo $message;
include_once("ng.html");
exit();
} else {
include_once("thanks.html");
}
form.html
<!DOCTYPE html>
<html>
<head>
</head>
<body class="contact">
<div class="ly__wrapper">
<div class="ly__con">
<h2 class="ttl-con__inner">お問い合わせ</h2>
<div class="form">
<form id="js-form" method="post" action="form.php">
<div>
<label>名前</label>
<input name="name" type="text" value="<?=h($viewModel['name']['value'])?>">
<div>
<?=h($viewModel['name']['error'])?>
</div>
</div>
<div>
<label>電話番号</label>
<input class="js-input" name="phone_number" type="text" value="<?=h($viewModel['phone_number']['value'])?>">
<div>
<?=h($viewModel['phone_number']['error'])?>
</div>
</div>
<div>
<label>メールアドレス / Email</label>
<input class="js-input" name="email" type="text" value="<?=h($viewModel['email']['value'])?>">
<div>
<?=h($viewModel['email']['error'])?>
</div>
</div>
<div>
<label>お問い合わせ内容</label>
<textarea name="message" rows="5"><?=h($viewModel['message']['value'])?></textarea>
<div>
<?=h($viewModel['message']['error'])?>
</div>
</div>
<div>
<input name="from_form" type="hidden" value="true">
<input id="js-recaptcha" name="g-recaptcha-response" type="hidden" value="">
<input type="hidden" name="csrf_token" value="<?=CsrfValidator::generate()?>">
<button id="js-submitBtn" type="button">送信</button>
<div id="js-submitError"></div>
</div>
</form>
</div>
</div><!-- /.ly__con -->
</div><!-- /.ly__wrapper -->
<script src="https://www.google.com/recaptcha/api.js?render=個別サイトキー"></script>//サイトキーは個人で取得
<script>
// Enter submit 禁止
(function() {
let inputs = document.getElementsByClassName("js-input");
let notEnterSubmit = function(e) {
if ((e.which && e.which === 13) || (e.keyCode && e.keyCode === 13)) {
event.preventDefault();
}
}
Array.prototype.forEach.call(inputs, function(input) {
input.addEventListener("keypress", notEnterSubmit);
});
window.addEventListener("unload", function() {
Array.prototype.forEach.call(inputs, function(input) {
input.removeEventListener("keypress", notEnterSubmit);
});
});
}());
// recapcha v3
(function() {
let form = document.getElementById("js-form");
let submitBtn = document.getElementById("js-submitBtn");
let submitError = document.getElementById("js-submitError");
let recaptcha = document.getElementById("js-recaptcha");
let click = function() {
grecaptcha.ready(function() {
grecaptcha.execute('個別サイトキー', {action: 'homepage'}).then(
function(token) { // トークン取得成功
console.log('recaptcha success');
recaptcha.value = token;
form.submit();
},
function(reason) { // トークン取得失敗
console.log('recaptcha failure');
event.preventDefault();
}
);
});
event.preventDefault();
};
submitBtn.addEventListener("click", click);
window.addEventListener("unload", function() {
submitBtn.removeEventListener("click", click);
});
}());
</script>
</body>
</html>
lib.php
フォームのバリデーションとリキャプチャ
<?php
abstract class FormItem
{
public $value;
public function __construct($value) {
$this->value = $value;
}
abstract protected function getMaxLength();
protected function nullOrEmpty() {
return empty($this->value);
}
protected function overLength() {
return mb_strlen($this->value) > $this->getMaxLength();
}
}
class Name extends FormItem
{
private static $max_length = 50;
public $error;
function __construct($str) {
parent::__construct($str);
if (parent::nullOrEmpty()) {
$this->error = '※ 入力されていません';
return;
}
if (parent::overLength()) {
$this->error = '※ 50文字以内で入力してください';
return;
}
}
protected function getMaxLength() {
return self::$max_length;
}
}
class PhoneNumber extends FormItem
{
private static $max_length = 20;
public $error;
function __construct($str) {
parent::__construct($str);
if (parent::nullOrEmpty()) {
$this->error = '※ 入力されていません';
return;
}
if (parent::overLength()) {
$this->error = '※ 20文字以内で入力してください';
return;
}
if ($this->incorrectFormat()) {
$this->error = '※ 電話番号の形式が正しくありません';
return;
}
}
protected function getMaxLength() {
return self::$max_length;
}
private function incorrectFormat() {
return preg_match("/[^0-9\+\-]+/", $this->value);
}
}
class Email extends FormItem
{
private static $max_length = 256;
public $error;
function __construct($str) {
parent::__construct($str);
if (parent::nullOrEmpty()) {
$this->error = '※ 入力されていません';
return;
}
if (parent::overLength()) {
$this->error = '※ 256文字以内で入力してください';
return;
}
if ($this->incorrectFormat()) {
$this->error = '※ メールアドレスが正しくありません';
return;
}
}
protected function getMaxLength() {
return self::$max_length;
}
private function incorrectFormat() {
return !preg_match("/[0-9a-z!#\$%\&'\*\+\/\=\?\^\|\-\{\}\.\_]+@[0-9a-z!#\$%\&'\*\+\/\=\?\^\|\-\{\}\.\_]+/", $this->value);
}
}
class Message extends FormItem
{
private static $max_length = 4000;
public $error;
function __construct($str) {
parent::__construct($str);
if (parent::nullOrEmpty()) {
$this->error = '※ 入力されていません';
return;
}
if (parent::overLength()) {
$this->error = '※ 400文字以内で入力してください';
return;
}
}
protected function getMaxLength() {
return self::$max_length;
}
}
class Recaptcha
{
private static $secret_key = 'reCAPTCHA サービスのシークレットキー';
public $response;
public $error;
function __construct($token) {
$url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . self::$secret_key . '&response=' . $token;
$option = [
CURLOPT_RETURNTRANSFER => true, // 文字列として返す
CURLOPT_TIMEOUT => 3, // タイムアウト時間
];
$ch = curl_init($url);
curl_setopt_array($ch, $option);
$json = curl_exec($ch);
$info = curl_getinfo($ch);
$errorNo = curl_errno($ch);
// OK以外はエラーなので空白
if ($errorNo !== CURLE_OK) {
// 詳しくエラーハンドリングしたい場合はerrorNoで確認
// タイムアウトの場合はCURLE_OPERATION_TIMEDOUT
return [];
}
// 200以外のステータスコードは失敗とみなし空配列を返す
if ($info['http_code'] !== 200) {
return [];
}
// 文字列から変換
$this->response = json_decode($json, true);
}
private function notHuman() {
return $this->response['success'];
}
}
CsrfValidator.php
<?php
class CsrfValidator {
const HASH_ALGO = 'sha256';
public static function generate() {
if (session_status() === PHP_SESSION_NONE) {
throw new \BadMethodCallException('Session is not active.');
}
return hash(self::HASH_ALGO, session_id());
}
public static function validate($token, $throw = false) {
$success = self::generate() === $token;
if (!$success && $throw) {
throw new \RuntimeException('CSRF validation failed.', 400);
}
return $success;
}
}
PageUtil.php
<?php
/**
* ウェブページ内で使われる関数
*/
/**
* Server Side Include の代わり
* @param <string> $path パス
*/
function ssi($path) {
include_once(filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $path);
}
function _ssi($path) {
return (filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $path);
}
/**
* タグ系の除去と改行の <br> 出力を行う。
* ただしモードを渡すことにより機能を変える。
* @param <string> $str 文字列
* @param <string> $mode モード
* plain ... htmlspacialchars や nl2br を介さず出力する
* price ... 数値の桁区切りを出力する
*/
function h($str, $mode = null) {
switch($mode) {
case 'price':
if (is_numeric($str)) {
$str = number_format($str);
}
break;
case 'plain':
return $str;
}
$str = htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
$str = nl2br($str);
return $str;
}
PHPMailerAutoload.php
<?php
/**
* PHPMailer SPL autoloader.
*
* @param string $classname The name of the class to load
*/
function PHPMailerAutoload($classname)
{
// Can't use __DIR__ as it's only in PHP 5.3+
$filename = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'class.' . strtolower($classname) . '.php';
if (is_readable($filename)) {
require $filename;
}
}
if (version_compare(PHP_VERSION, '5.1.2', '>=')) {
// SPL autoloading was introduced in PHP 5.1.2
if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
spl_autoload_register('PHPMailerAutoload', true, true);
} else {
spl_autoload_register('PHPMailerAutoload');
}
} else {
/**
* Fall back to traditional autoload for old PHP versions.
*
* @param string $classname The name of the class to load
*/
function __autoload($classname)
{
PHPMailerAutoload($classname);
}
}
ポイント
・PHPで書かれているのでPHP読み込まれているかの確認が必要。
・PHPのライブラリはComposerはPHPのパッケージ管理ツールがありそれをつかってPHPmailerを読み込むのが定番のよう。公式からのダウンロードもあるがイマイチそのまま使用できる感じではなく説明書みたいになっている。
セキュリティー関連の追加を知っている方がいい。
値セット箇所
$from = ""; // 送信者欄
$smtp_user = ""; //送信者
$smtp_password = ""; //SMTPサーバーのパスワード
$to = ""; //宛先 受信者
✔︎$from =””;
「送信者欄」問合せフォーム機能として利用しているメールサーバーのメールアドレスを設定する。お問い合わせメールを受け取った側が誰から届いたか確認する箇所を決める。Gmailを利用してる筆者はgmailアドレスここに設定している。
【例】xxxxxxx.gmai.com
✔︎$smtp_user =””;
【例】xxxxxxx.gmai.com
「送信者」送信者なので送信者欄とかぶってややこしい。代入先の$smtpを見た方が理解しやすい。SMTP(Simple Mail Transfer Protocol)のユーザー設定。gmailの場合はSMTP機能をGmailで担っているもでgmail。筆者は$fromと同じアドレスを設定している。
✔︎$smtp_password =””;
【例】gmailのログインパス
「SMTPサーバーのパスワード」上記で$smtp_userがある。そのパスワードを設定する。筆者はGmailをSMTPとして利用しているのでgmailのパスをここでは設定。※gmail2段階認証の場合は通常設定ではログインできないのでこの件はスミマセン調べてみてください。
✔︎$to =””;
「宛先 受信者」メールを受信したい場所。このメール機能をそのまま受け口として送信と同じアドレス設定にすることもできそうですが、メール送信機能のみで使用するのがいいかもしれません。(この辺りはどちらがいいか不明、未調査)実際筆者は別のアドレス先に送りたいので受信先を別に設定しています。また、同じサイトにある複数のお問い合わせフォームはそれぞれお問い合わせ先(宛先)が違ったので、メールサーバーは一つ(送信者)で宛先をフォームごとに変えて設定していました。
セットアップについて
大まかな流れ
➀Composerを導入
PHP MailerはPHPで電子メールを送信するためのライブラリ。そのライブラリ利用するためにはPHP のプロジェクトが必要とするライブラリやパッケージを管理する「ライブラリ依存管理ツール」必要になります。そのためライブラリパッケージ管理「Composer」をはじめに入れます。
✔︎【Windows】公式ダウンロード
公式:https://getcomposer.org/
「Download」ボタン→Composer-Setup.exe
✔︎【Windows】インストール
指示に従ってインストール
インストール後はインストールされたのかターミナル(またはコマンドプロンプト)でcomposerコマンドを使う。バージョンンストールした日時などが表示されれば完了です。デカくComposerとでるっぽい。
➁Composerでパッケージの処理を定義しておくファイルを作成
✔︎composer.jsonとcomposer.lock
【composer install コマンド】で生成される。composer.jsonを手動で作成してもOK。
➂Composer で PHP Mailer をインストール
✔︎composer.jsonに以下を書きこむ
{
"require": {
"phpmailer/phpmailer": "~6.1"
}
}
✔︎インストール
【インストールコマンド】
composer require phpmailer/phpmailer
ーーーーーーー【困ったら】ーーーーーーーー
【出来なけれはComposerのインストールされているディレクトリで上記のコマンドを試す】
【composerのパスが通っていない場合、composer.phar のあるディレクトリで以下を実行】
php composer.phar require phpmailer/phpmailer
【phpのバージョンが低い場合は、新しいPHPのパスを調べ以下のように実行】
/usr/bin/phpX.X composer.phar require phpmailer/phpmailer
✔︎ファイルを設置
form.phpとform.htmlを中心に記述し、インポート系の関連ファイルも作成。
この辺りは設置して実践したらまた詳しく追記したいと思います。現在は既存のサイトを設定するか同じような設定なので使いまわしているためゼロからは未経験。今度自分で実践予定です。
あとがき
もしかしたら、足りないファイルなどあれば申し訳ありません。このあたりの設定を詳しくしりたくて読んだり関連ファイルを見つけたりちょっと設定担当したりだったのですが問合せフォームの機能ってGoogle使ってたんだ~と驚いたのと、これは筆者の環境の話ですが、そもそも機能自体を多数同じ管轄内では使い回してるのにも、なるほどー新たな発見ばかりでした。
便利なライブラリはもっと触っていくべきだし、バックエンド系もやらねば!と思うのでした。
参考URL
https://qiita.com/takamat444/items/14385414a3f787ba8574
https://blog.megefeps.info/20190618/post-1498/
https://www.webdesignleaves.com/pr/plugins/google_recaptcha.php
https://web.hazu.jp/php-mailer/
https://www.sejuku.net/blog/82454
https://m-tomoya.org/compose-json_composer-lock/
https://web.hazu.jp/php-mailer/