- 分類: 軟體文章
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)
<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)
<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 畫面編輯回應項目,填寫管理者回覆。
回應表單加入圖形驗證碼審核
回應系統方便訪客提出反饋,不過方便不是隨便:要是被機器人程式盯上,三不五時塞了一堆垃圾回應那就不好了。
做為防禦機制,我申請 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 以域名為單位,在本機環境測試時記得需將圖形驗證規則註記後回應才能送出。
通知信件怎麼寄?
完成圖形驗證碼導入後我才想到:回應功能沒有通知機制,我需要在有新回應時收到回應通知信。
電子郵件程式寄信時使用 SMTP 協定,透過 SMTP 伺服器寄發出去。在 Outlook 或是 ThunderBird 這類電子郵件程式設定寄件資訊時,你需要輸入 SMTP 伺服器名稱、通訊埠口、使用者帳號及密碼,Laravel 專案要寄通知信也需要上述資料。
GMail 作為 SMTP 服務:需申請應用程式密碼
GMail 是由 Google 提供的電子郵件服務,你可以透過行動載具 app 或是瀏覽器使用服務收發信件。同時 GMail 可以作為 SMTP 伺服器給 Laravel 專案使用,設定時需要「應用程式密碼」。
-
前往 GMail 設定,在「轉寄和 POP/IMAP」中確認「IMAP 存取」功能的啟用。
- 到 Google 帳戶(https://myaccount.google.com)登入,點選左方選單「安全性」,找到「登入 Google」區塊後先啟動「兩步驟驗證」提高安全性。
-
在同一區塊點選「應用程式密碼」。
-
在「選擇應用程式」下拉選單選擇「其他」,輸入識別資訊後按下「產生」,你會在 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 主要功能(除了搜尋)都已經到位,通知信功能的建立也能應用在聯絡表單上,如果搭配隊列與排程也可應用在電子報寄發,不過那個是另一件事了。
接下來想在內容呈現部分加些東西,讓使用者更易於使用網站功能,增進使用者體驗。