A+ A A-

larablog 建構日誌:回應與通知信

  在社群網站還沒流行的時候,電子郵件、留言板及回應系統是部落格主跟訪客互動的常見機制,在文章下方的回應系統讓訪客能對該篇文章提出回饋,小巧且方便。

  將 ffxitoolbox 網站以 Laravel 改寫時對這個框架還不甚了解,加上網站定位是合成資料搜尋的關係所以僅實做留言板系統。到了對 Laravel 有更多瞭解的現在,透過實做回應系統讓 larablog 更貼近實際運作的部落格平台。

建立回應的 Controller

  儲存回應內容的資料庫部分在〈larablog 建構日誌:專案資料表〉部分已經做好了,在 Model 的關聯設定則是歸屬(belongsTo)於 Post Model 的反向多對一:一篇文章會有多篇回應。

  使用的欄位除 id 與建立、更新時間之外定義以下項目:

  • post_id:做為與 posts 資料表關聯的外鍵,
  • name:訪客名字。
  • email:訪客電郵,與訪客姓名同為必須欄位。
  • website:訪客如有網站的話可在此輸入網址。
  • comment:回應內容,必須欄位。為了避免有人做奇怪的事此欄位禁用 HTML 標籤,而後透過 nl2br 函式處理內容斷行。
  • admin_reply:管理者回覆,可使用 HTML 標籤。

  有了資料表,現在建立對應的 Controller:CommentController

php artisan make:controller CommentController

  我在 CommentController 建立 addComment 方法,處理新增回應時要做的事。

 <?php
namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class CommentController extends Controller
{
    public function addComment($slug){
        $post = Post::where('slug', $slug)->first();
        $inputs = request()->all();

        // 驗證規則
        $rules = [
                'name' => ['required', 'max:150'], //訪客名字
                'email' => ['required', 'max:150'], //電子郵件
                'comment' => ['required'], //回應內容
                ];

        // 驗證資料
        $validator = Validator::make($inputs, $rules);
        // 資料驗證錯誤時的處理
        if ($validator->fails()){
            return redirect()
            ->withErrors($validator)
            ->withInput();
        }

        $inputs['comment'] = nl2br($inputs['comment']);
        $inputs['post_id'] = $post->id;
        $comment = Comment::create($inputs);

        return redirect()->back();
    }

}

  當訪客透過回應表單填寫內容送出回應時,會先檢查必填欄位是否填寫資料,同時設定驗證規則做基本把關。引入 Post Model 以獲得回應發佈時所在的文章資訊,將文章的 id 寫入回應資料表的 post_id 欄位。

PostCotroller 修訂

  回應內容及填寫表單是在文章全文畫面,所以要在 renderBlogPage 方法中加入回應內容變數:

$with_comments = $post->comments->sortByDesc('created_at');

  回傳到 Blade template 的部分也要加入 $with_comments 變數:

return view('blog_page', $bindings, compact('post', 'with_comments'));

  renderBlogByTag 方法的 $bindings 陣列加入:

'comments_count' => Comment::where('post_id','$post->id')->count(),

既有 Blade template 的修改

  首頁、分類文章、標籤篩選文章及文章全文頁面中都會列出文章中的回應計數,先前這部分尚未實做,現在該補上了:利用 Eloquent 語法,以 count 方法計算屬於該文章的回應數。

  以下是分類文章 Blade template 中關於回應計數的敘述,其他頁面也一樣:

<li class="d-flex align-items-center"><i class="bi bi-chat-dots"></i>
	<a href="/blog/{{ $post->slug }}#comments">{{ $post->comments->count() }} 回應</a>
</li>

回應用 Blade template 檔案

  為了讓回應的呈現方式更簡單些,我把 Moderna 主題的回應範例修改成較簡略的版本。表單部分加上提醒文字以及〈服務條款〉、〈隱私權政策〉連結,以 Blade template 檔案的方式引入文章全文畫面。

// /resources/views/widgets/with_comments.blade.php

<div class="blog-comments">
    @if($post->comments->count() == 0)
        <h4  class="comments-count">看完文章有什麼想法嗎?利用下面表單告訴作者吧!</h4>
    @else
        <h4 class="comments-count">{{ $post->comments->count() }} 則回應</h4>
    @endif

    @foreach($with_comments as $with_comment)
    <div class="comment">
        <div class="d-flex">
            <div class="comment-img"><img src="{{ asset('img/comment_user.png') }}" alt=""></div>
            <div>
                @if($with_comment->website != '')
                    <h5><a href="{{ $with_comment->website }}" target="_blank">{{ $with_comment->name }}</a>
                        @if(session()->has('role_id') && session('role_id') == 1)
                            &nbsp;&nbsp;<a href="/comment/{{ $with_comment->id }}" onClick="return confirm('確定要刪除嗎?')">刪除</a>
                        @endif
                    </h5>
                @else
                    <h5>{{ $with_comment->name }}
                        @if(session()->has('role_id') && session('role_id') == 1)
                            &nbsp;&nbsp;<a href="/comment/{{ $with_comment->id }}" onClick="return confirm('確定要刪除嗎?')">刪除</a>
                        @endif
                    </h5>
                @endif
                <time datetime="{{ $with_comment->created_at->toDateTimeString() }}">{{ $with_comment->created_at->diffForHumans() }}</time>
                <p>
                    {!! $with_comment->comment !!}
                </p>
            </div>
        </div>
    </div>
    @if($with_comment->admin_reply != '')
        <div class="comment-reply">
            <div class="d-flex">
                <div class="comment-img"><img src="{{ asset('img/abo_admin_reply.png') }}" alt="管理者回覆"></div>
                <div>
                    <h5>管理者回覆</h5>
                    <p>
                        {!! $with_comment->admin_reply !!}
                    </p>
                </div>
            </div>
        </div>
    @endif
    @endforeach

    <div class="reply-form">
        <h4>張貼回應</h4>
        <p>請先閱讀<a href="/term">服務條款</a>及<a href="/privacy-policy">隱私權政策</a>,送出回應意即同意前述文件。標記 * 欄位請務必填寫,電子郵件信箱不會顯示</p>
        <form action="/comment/add/{{$post->slug}}" method="POST">
            @csrf
            <div class="row">
                <div class="col-md-6 form-group">
                    <input name="name" type="text" class="form-control" placeholder="你的名字*">
                </div>
                <div class="col-md-6 form-group">
                    <input name="email" type="text" class="form-control" placeholder="電子郵件*">
                </div>
            </div>
            <div class="row">
                <div class="col form-group">
                    <input name="website" type="text" class="form-control" placeholder="網站網址,請加上 http:// 或 https://">
                </div>
            </div>
            <div class="row">
                <div class="col form-group">
                    <textarea name="comment" rows="4" class="form-control" placeholder="回應內容*"></textarea>
                </div>
            </div>
            <button type="submit" class="btn btn-primary">張貼回應</button>
        </form>
    </div>
</div><!-- End blog comments -->

增加路由

  在 /routes/web.php 加上增加回應的路由後就可以測試回應系統運作了。

// 回應操作
Route::post('/comment/add/{slug}', 'App\Http\Controllers\CommentController@addComment');

回應內容及回應表單

管理者回覆

  在 Voyager 建立 comments 資料表的 BREAD 之後就可以在 Voyager 畫面編輯回應項目,填寫管理者回覆。

在 Voyager 畫面填寫管理者回覆

前端顯示訪客回應及管理者回覆

回應表單加入圖形驗證碼審核

  回應系統方便訪客提出反饋,不過方便不是隨便:要是被機器人程式盯上,三不五時塞了一堆垃圾回應那就不好了。

  做為防禦機制,我申請 hCAPTCHA 圖形驗證碼服務。

申請 hCATPCHA 服務,取得連線金鑰

  CAPTCHA(圖形驗證碼)是什麼?白話的解釋就是:驗證操作的是人還是機器的一種機制。使用者透過閱讀圖形驗證碼顯示的驗證規則,點選正確的圖片或是訊息送出,驗證碼機制會回傳判定結果:如果正確則回傳「成功」訊息讓使用者填寫的資料能送出;錯誤則回傳另一題重複前述動作。

  Google reCAPTCHA 提供免費的圖形驗證支援,以域名為單位(支援泛用域名)申請溝通用的網站金鑰(或稱公開金鑰,與 Google reCAPTCHA 聯絡用)及私密金鑰(妥善保管不能公開)資料,開發者利用這兩筆資料與 Google reCAPTCHA 溝通。

  hCAPTCHA 是另一個知名的圖形驗證碼服務,申請流程跟 Google reCAPTCHA 差不多,本文以 hCAPTCHA 為例實做回應表單的圖形驗證碼檢查。

申請服務及安裝套件

  hCAPTCHA 的網址是:https://www.hcaptcha.com,註冊後取得網站 Sitekey (公開金鑰,各網站分開)與 Secretkey(在 Settings 分頁取得,各網站共用)。

  在 .env 填入申請到的金鑰資訊後存檔。

HCAPTCHA_SECRET=(從 hCAPTCHA 取得的 Secretkey)
HCAPTCHA_SITEKEY=(從 hCAPTCHA 取得的 Sitekey)

  安裝套件:

composer require scyllaly/hcaptcha

  編輯 /config/app.php ,在表列 ServiceProvider 陣列中加入:

Scyllaly\HCaptcha\HCaptchaServiceProvider::class,

  在表列別名陣列中加入:

'HCaptcha' => Scyllaly\HCaptcha\Facades\HCaptcha::class,

  然後發佈檔案:

php artisan vendor:publish --provider="Scyllaly\HCaptcha\HCaptchaServiceProvider"

引用 hCAPTCHA

  在回應表單的 Blade template 檔案最前頭加上

 {!! HCaptcha::renderJs() !!} 

  引入 hCAPTCHA JavaScript 資源,如要以正體中文顯示請改為

 {!! HCaptcha::renderJs('zh-TW') !!}

  接著在要顯示驗證碼的地方(通常是「送出」按鈕的前面或上方)填入

{!! HCaptcha::display() !!}

  完成前端配置。

驗證規則加上 hCAPTCHA

  後端部分編輯 CommentController,在 $rules 陣列中加入 hCAPTCHA 加入驗證:

// 驗證規則
$rules = [
					'guestName' => ['required', 'max:150'], //訪客名字
					'guestSubject' => ['required', 'max:150'], //留言標題
					'guestComment' => ['required'], //留言內容
					'h-captcha-response' => ['required|HCaptcha'], //圖型驗證
         ];

導入 hCAPTCHA 後的回應表單

  加入後使用者除了填寫必要欄位外,還需要通過 hCAPTCHA 驗證才能夠送出回應。因為 hCAPTCHA 以域名為單位,在本機環境測試時記得需將圖形驗證規則註記後回應才能送出。

通知信件怎麼寄?

  完成圖形驗證碼導入後我才想到:回應功能沒有通知機制,我需要在有新回應時收到回應通知信。

  電子郵件程式寄信時使用 SMTP 協定,透過 SMTP 伺服器寄發出去。在 Outlook 或是 ThunderBird 這類電子郵件程式設定寄件資訊時,你需要輸入 SMTP 伺服器名稱、通訊埠口、使用者帳號及密碼,Laravel 專案要寄通知信也需要上述資料。

GMail 作為 SMTP 服務:需申請應用程式密碼

  GMail 是由 Google 提供的電子郵件服務,你可以透過行動載具 app 或是瀏覽器使用服務收發信件。同時 GMail 可以作為 SMTP 伺服器給 Laravel 專案使用,設定時需要「應用程式密碼」。

  1. 前往 GMail 設定,在「轉寄和 POP/IMAP」中確認「IMAP 存取」功能的啟用。
    GMail 的 IMAP 設定
  2. 到 Google 帳戶(https://myaccount.google.com)登入,點選左方選單「安全性」,找到「登入 Google」區塊後先啟動「兩步驟驗證」提高安全性。
  3. 在同一區塊點選「應用程式密碼」。
    Google 帳戶設定中的「 安全性 - 應用程式密碼」
  4. 在「選擇應用程式」下拉選單選擇「其他」,輸入識別資訊後按下「產生」,你會在 Modal 視窗中獲得一組 16 個英數字組合的密碼,請務必截圖或以其他方式紀錄下來,因為離開畫面後無法再次觀看。
    申請應用程式密碼

  接著到 .env 檔案儲存 GMail 的 SMTP 服務資訊:

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=(申請的 GMail 信箱)
MAIL_PASSWORD=(申請的應用程式密碼)
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=(寄信者信箱)
MAIL_FROM_NAME="${APP_NAME}"

  如果網站是建置在 Google 雲端平台(Google Cloud Platform,GCP),你可能會發現無法使用 GMail 寄信。這是因為 GCP 不開放使用者使用 TCP 埠號 465 及 587,此時請改用像是 Mailgun 這類專業 SMTP 發信業者的服務,以自訂埠號傳輸。

建立信件寄發作業

  以下內容參考 Laravel 8 中文文檔(簡體中文):https://learnku.com/docs/laravel/8.x/mail/9395

安裝套件

  下載 Guzzle HTTP 客戶端函式庫

composer require guzzlehttp/guzzle

  過程中如果出現版本鎖定訊息請在指令後加上 --with-all-dependencies 參數。

建立寄信類別

  建立給回應功能寄信的 Mail 類別,建立的 Mail 類別檔案會位於 app/Mail 資料夾。

php artisan make:mail CommentMailer

  在類別中做兩件事:在建構子中宣告儲存通知信內容的陣列變數 $mailDetails,以及在 build 方法中定義取鍵 subject 的值作為通知信標題,並以 Blade template 檔案 resources/views/email/commentNotification.blade.php 作為信件版型。

<?php
// app/Mail/CommentMailer.php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class CommentMailer extends Mailable
{
    use Queueable, SerializesModels;

    public $mailDetails;
    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($mailDetails)
    {
        $this->mailDetails = $mailDetails;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->subject($this->mailDetails['subject'])->view('email.commentNotification');
    }
}

回應 Controller 補足通知信內容

  在 CommentController 用命名空間將郵件類別以及 User Model 拉進來。

use App\Mail\CommentMailer;
use App\Models\User;
use Illuminate\Support\Facades\Mail;

  將回應資料填入 $mailDetails 陣列:郵件主題、回應對應的文章、訪客名及電子郵件、回應發佈時間(發佈當下),以及回應文字。

//通知信件
$mailDetails=[
	'subject' => '【通知】larablog 有訪客填寫回應',
	'post' => $post->title,
	'guest' => $inputs['name'] . '<' . $inputs['email'] . '>',
	'commentTime' => now(),
	'comment' => $inputs['comment'],
	];

  定義管理者信箱 $adminEmail

$adminEmail = User::find(1)->email;

  在回應資料建立後將信件寄送到管理者信箱:

Mail::to($adminEmail)->send(new CommentMailer($mailDetails));

信件用的 Blade template

  以 Blade template 定義信件外觀能夠豐富多變化,以我自己的情況只要知道是哪篇文章的回應即可,所以走簡約風(笑)。

{{-- resources/views/email/commentNotification.blade.php --}}
<h2>文章:{{ $mailDetails['post'] }}</h2>
<p>訪客:{{ $mailDetails['guest'] }}</p>
<p>時間:{{ $mailDetails['commentTime'] }}</p>
<p>回應:{{ $mailDetails['comment'] }}</p>

收到的回應通知信

結語

  加入回應功能後 larablog 主要功能(除了搜尋)都已經到位,通知信功能的建立也能應用在聯絡表單上,如果搭配隊列與排程也可應用在電子報寄發,不過那個是另一件事了。

  接下來想在內容呈現部分加些東西,讓使用者更易於使用網站功能,增進使用者體驗。