こんにちは、Ryohei(@ityryohei)です!

本記事は、Vue.jsとPHPでお問い合わせフォームを作ってみよう!というチュートリアルです。Vueを触り始めた方に向けた内容となっております。範囲はプロジェクトの作成~メールの送信までです。

※サーバーサイドはPHPを使用しますので、予めご了承ください。

Vue.jsとPHPでお問い合わせフォームを作るにはどうすればいいんだろう?

上記の疑問にお答えします。

では、解説していきます。

本記事で構築するもの

本記事では下記スライドのように確認画面や送信画面がない1ページで完結するお問い合わせフォームを構築します。フロントはVue.js、サーバーサイドはPHPを使用。一応、正しいメールアカウントを設定することでメールの送信も可能となっています。

注意点

本記事の主目的がメールの送信なので、構築する上で省略している部分が多々あります。特にバリデーションチェックについてはフロント側で簡単なものしか組んでいません。サーバー側なんてぱっぱらぱーの状態なのでその点ご注意ください!

開発環境はざっくり下記のような感じです。バージョンによっては動作しない可能性がございます。また、Node.jsやVue.jsのインストールにつきましては解説しておりませんので、予めご了承ください。

  • OS:Windows 10
  • Vue CLI:4.5.11
  • PHP:7.4.1
  • サーバー:XAMPP + Apache

前置きはこれくらいにしておきまして、本題に戻ります。

プロジェクトを作成する

まずはVueプロジェクトの作成から。プロジェクトを作成するディレクトリに移動します。

cd /xampp/htdocs/vue

対象のディレクトリに移動したら、下記のコマンドを実行してプロジェクトを作成しましょう。ここではプロジェクト名は「contact」にしています。

$ vue create contact

コマンドを実行するとプリセットが表示されますので、下記のものを選択。ログが流れますのでしばし待ちます。

Vue CLI v4.5.11
? Please pick a preset: . ([Vue 2] babel, router, eslint)
Vue CLI v4.5.11
Creating project in C:\xampp\htdocs\vue\contact.
Installing CLI plugins. This might take a while...
added 1278 packages in 2m
Invoking generators...
Installing additional dependencies...
added 52 packages in 8s
Running completion hooks...
Generating README.md...
Successfully created project contact.
Get started with the following commands:
 $ cd contact
 $ npm run serve

「Successfully~」で成功です。プロジェクトが作成できたら、表示に従いコマンドを実行してプロジェクトを実行します。

C:\xampp\htdocs\vue>cd contact
C:\xampp\htdocs\vue\contact>npm run serve
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.11.9:8080/

表示されたアドレスにアクセスするとVueのプロジェクトページが表示されます。以上でプロジェクトの作成は完了です。

お問い合わせページを作成する

ではお問い合わせページを作っていきます。構築手順はエンジニアによって異なると思いますが、個人的には最初に表示を確認できるようにしておきたいので、ビューの作成とルートの登録から始めます。

ビューを作成する

下記のディレクトリに「Contact.vue」ファイルを作成。タイトルだけ表示するようにしておきます。

ファイル:xampp > htdocs > vue > contact > src > views > Contact.vue
<template>
  <div class="contact">
    <h2>お問い合わせ</h2>
  </div>
</template>

index.jsにルートを登録する

続いてルートを登録します。「index.js」に「contact」を追加しましょう。(しれっとmodehistoryにしています。)

ファイル:xampp > htdocs > vue > contact > src > router > index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Contact from '../views/Contact.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/contact',
    name: 'Contact',
    component: Contact
  },
]
const router = new VueRouter({
  mode: 'history',
  routes
})
export default router

ここまで設定が終わったら下記のアドレスにアクセスしてみましょう。「お問い合わせ」と出力されたページが表示されるかと思います。

http://localhost:8080/

一先ずお問い合わせページを作ることができました!後はこのファイルを編集して、冒頭でご紹介したお問い合わせフォームを作っていきます!

と、その前に。

非同期通信の準備

本記事では、フォームで入力した内容を非同期でPHPに渡します。非同期通信を行うモジュールは選択肢がいくつかあると思いますが、ここではaxiosを使用しますので、インストールとインポートを済ませておきます。

axiosをインストールする

プロジェクトディレクトリに移動して下記コマンドを実行してモジュールをインストールします。

$ npm install --save axios vue-axios

main.jsにaxiosをインポートする

axiosを使用するためにちょっと追記。

ファイル:xampp > htdocs > vue > contact > src > main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios' //←追加
import VueAxios from 'vue-axios' //←追加
Vue.config.productionTip = false
Vue.use(VueAxios, axios) //←追加
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

以上でモジュールのインストール・インポートは完了です。コマンドでさくっとインストールできるので楽ちんですね!

フォームを作成する

事前準備は諸々完了!ここからが本題です。

前項で作成した「Contact.vue」にフォームを構築していきます。Vue.jsはHTML、CSS、JSを一つのファイルにまとめることができますので、フロント部分は「Contact.vue」にすべて記述します。

PHPは別ディレクトリに作成。ローカル環境ではお問い合わせページとAPIでオリジンが異なるため、CORSに注意する必要があります。

  • Vue.js:http://localhost:8080/contact
  • PHP(XAMPP):http://localhost/api/contact/send.php

以上を踏まえまして、コーディングしていきます。

HTML

ファイル:xampp > htdocs > vue > contact > src > views > Contact.vue
<template>
    <div id="app" class="contact">
        <h2>お問い合わせ</h2>
        <form class="contact-form" method="post" action="">
            <table>
                <tbody>
                    <tr>
                        <th>お名前</th>
                        <td>
                            <input v-model="name" placeholder="例)田中 太郎">
                            <p class="error">{{ errors.name }}</p>
                        </td>
                    </tr>
                    <tr>
                        <th>フリガナ</th>
                        <td>
                            <input v-model="kana" placeholder="例)タナカ タロウ">
                            <p class="error">{{ errors.kana }}</p>
                        </td>
                    </tr>
                    <tr>
                        <th>電話番号</th>
                        <td>
                            <input v-model="tel" placeholder="例)0120-000-000">
                            <p class="error">{{ errors.tel }}</p>
                        </td>
                    </tr>
                    <tr>
                        <th>メールアドレス</th>
                        <td>
                            <input v-model="email" placeholder="例)info@example.co.jp">
                            <p class="error">{{ errors.email }}</p>
                        </td>
                    </tr>
                </tbody>
            </table>
            <button v-if="!valid" :disabled="!valid" type="button">未入力の必須項目があります</button>
            <button v-else-if="valid" type="button" @click="submit">送信</button>
        </form>
        <transition name="fade">
            <div v-if="result" class="contact-result">{{ result }}</div>
        </transition>
    </div>
</template>

CSS

ファイル:xampp > htdocs > vue > contact > src > views > Contact.vue
<style scoped>
div, p, h2, ul, li {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
.contact {
    width: 100%;
    max-width: 600px;
    padding: 28px;
    margin: 0 auto;
    background-color: #eeeeee;
}
.contact h2 {
    margin: 0 0 28px 0;
}
.contact-form table {
    width: 100%;
    margin: 0 0 28px 0;
    border-collapse: collapse;
}
.contact-form table tr {
    border-bottom: 2px solid #cccccc;
}
.contact-form table tbody tr th {
    width: 25%;
    padding: 20px 10px;
    text-align: left;
}
.contact-form table tbody tr td {
    width: 75%;
    padding: 20px;
    text-align: left;
}
.contact-form table tbody tr td input {
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
}
.contact-form .error {
    margin: 0;
    color: #ff0000;
    font-weight: bold;
    font-size: .85rem;
}
.contact-form button {
    display: block;
    width: 300px;
    padding: 10px 0;
    margin: 0 auto;
    color: #ffffff;
    border: 0;
    box-shadow: none;
    background-color: #384878;
    cursor: pointer;
}
.contact-form button:disabled {
    background-color: #a5a5a5;
}
.contact-result {
    padding: 5px 0;
    margin: 28px auto 0 auto;
    color: #ffffff;
    box-sizing: border-box;
    background-color: #00c2bc;
}
.fade-enter-active, .fade-leave-active {
    transition: opacity .7s;
}
.fade-enter, .fade-leave-to {
    opacity: 0;
}
</style>

JS

ファイル:xampp > htdocs > contact > src > views > Contact.vue
<script>
import axios from 'axios'
export default {
    data: function() {
        return {
            name: '',
            kana: '',
            tel: '',
            email: '',
            result: '',
        }
    },
    computed: {
        chackName: function(){
            if(!this.name){
                return 'お名前を入力してください';
            }
            return '';
        },
        chackKana: function(){
            if(!this.kana){
                return 'フリガナを入力してください';
            } else if(!this.validKatakana(this.kana)){
                return 'フリガナをカタカナで入力してください';
            }
            return '';
        },
        checkTel: function(){
            if(!this.tel){
                return '電話番号を入力してください';
            } else if(!this.validTel(this.tel)){
                return '電話番号を正しく入力してください';
            }
            return '';
        },
        chackEmail: function(){
            if(!this.email){
                return 'メールアドレスを入力してください';
            } else if(!this.validEmail(this.email)){
                return 'メールアドレスを正しく入力してください';
            }
            return '';
        },
        errors: function() {
            const errors = {
                'name': this.chackName,
                'kana': this.chackKana,
                'tel': this.checkTel,
                'email': this.chackEmail,
            };
            for (var key in errors) {
                if (errors[key] === '' || errors[key] === null || errors[key] === undefined) {
                    delete errors[key];
                }
            }
            return errors;
        },
        valid: function() {
            return !Object.keys(this.errors).length;
        }
    },
    methods: {
        submit: async function(){
            const result = await this.send();
            this.result = result;
            if(result === '送信完了'){
                this.clear();
            }
        },
        send: async function(){
            const url = 'http://localhost/api/contact/send.php';
            const params = {
                'name': this.name,
                'kana': this.kana,
                'tel': this.tel,
                'email': this.email
            }
            return await axios
                .post(url, params)
                .then(function(res){
                    return res.data;
                })
                .catch(function(error){
                    console.log(error);
            });
        },
        validKatakana: function(kana) {
            const re = /^[ァ-ンァ-ン\x20\u3000゙゚]*$/;
            return re.test(kana);
        },
        validTel: function(tel) {
            const re = /^(0[5-9]0[0-9]{8}|0[1-9][1-9][0-9]{7})$/;
            return re.test(tel);
        },
        validEmail: function(email) {
            const re = /^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
            return re.test(email);
        },
        clear: function(){
            this.name = '';
            this.kana = '';
            this.tel= '';
            this.email = '';
        }
    }
}
</script>

PHP

ファイル:xampp > htdocs > api > contact > send.php
<?php
header("Access-Control-Allow-Origin: http://localhost:8080");
header("Access-Control-Allow-Headers: Content-Type");
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require "vendor/autoload.php";
$json = file_get_contents("php://input");
$inputs = json_decode($json, true);
$msg = "";
//メール本文
foreach($inputs as $key => $value){
    $msg .= $key. ":". $value . "\n";
}
$mail = new PHPMailer(true);
try {
    $host = "HOST"; //メールサーバーホスト
    $username = "SMTP_USERNAME"; // SMTPユーザー
    $password = "SMTP_PASSWORD"; //SMTPパスワード
    $from = "FROM_MAIL_ADDRESS"; //差出人メールアドレス
    $fromname = "FROM_NAME"; //差出人の名前
    $to = "TO_MAIL_ADDRESS"; //送信先メールアドレス
    $toname = "TO_NAME"; //送信先の名前
    //件名・本文
    $subject = "送信テスト";
    $body = $msg;
    //メール設定
    $mail->isSMTP();
    $mail->SMTPAuth = true;
    $mail->Host = $host;
    $mail->Username = $username;
    $mail->Password = $password;
    $mail->SMTPSecure = "tls";
    $mail->Port = 587;
    $mail->CharSet = "utf-8";
    $mail->Encoding = "base64";
    $mail->setFrom($from, $fromname);
    $mail->addAddress($to, $toname);
    $mail->Subject = $subject;
    $mail->Body    = $body;
    //メール送信
    if($mail->send()){
        echo "送信完了";
    }
} catch (Exception $e) {
    echo "送信失敗";
}
exit();

かなり長くなりましたがここまでがソースの全容になります。構築時には細かくコメントを記載していたのですが、長くなるというのと、そこまで複雑な処理ではないという理由で全部削除して掲載しています。

また、全く触れていませんでしたが、PHPのメール送信には「PHPMailer」というライブラリを使用しています。詳しくは下記のページでご紹介していますので、参考にしていただければ!

最後に

Vue.jsの勉強もかねてお問い合わせフォームを作ってみました。夜中に始めて朝方完成しましたが、久しぶりに楽しくコーディングできた気がします。

お問い合わせフォームは入門にうってつけのアプリケーションだと思いますので、これからVue.jsを始めようかな?と考えている方の参考になれば幸いです!

以上、Vue.js+PHPでお問い合わせフォームを作ってみよう!のご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。