傳統寄送 email 是採用同步的方式,也就是當你寄出一封信,必須等 email server 回應後,才可以繼續後續的程式動作,因此使用者會有明顯的等待時間;若能搭配 queue 機制,寄送 email 後,馬上以非同步的方式回到原來程式繼續執行,會有另外一個 process 去消耗 queue,負責寄送 email。
Motivation
Laravel 的 Mail::send()
是以同步方式寄送 email,會有明顯的等待時間;而 Mail:queue()
則是以非同步的方式寄送 email,速度非常快,不過必須搭配 queue 以及其他配套機制。
Version
PHP 7.0.8
Laravel 5.2.45
實際案例
先以 Mail::send()
透過 Gmail 寄送信件,然後再改用 Mail::queue()
方式寄送信件。
單元測試 : 由 Sync 寄送信件
MailServiceTest.php 1 1 GitHub Commit : 單元測試 : 由 sync 寄送信件
use App\Services\MailService; class MailServiceTest extends TestCase { /** @test */ public function 由Sync寄送信件() { /** arrange */ /** act */ App::call(MailService::class . '@mailBySync'); /** assert */ $this->assertTrue(true); } }
這裡沒做任何測試,只是透過 PHPUnit 啟動 sync 方式寄送信件。
設定使用 Gmail 寄信
.env 2 2 GitHub Commit : 設定使用 Gmail 寄信
APP_ENV=local APP_DEBUG=true APP_KEY=base64:PABSMLELX37jW2jJdwm9Fk6LvUWupulQXAWDZcfA7xE= APP_URL=http://localhost DB_CONNECTION=sqlite DB_HOST=127.0.0.1 DB_PORT=3306 #DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_DRIVER=sync REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME=[your gmail address] MAIL_PASSWORD=[your password] MAIL_ENCRYPTION=tls
21 行
MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME=[your gmail address] MAIL_PASSWORD=[your password] MAIL_ENCRYPTION=tls
-
MAIL_HOST
: 設定為
smtp.gmail.com
。 -
MAIL_PORT
: 設定為
587
。 -
MAIL_USERNAME
: 設定你的 Gmail 帳號,如
example@gmail.com
。 - MAIL_PASSWORD : 設定你的 Gmail 密碼。
-
MAIL_ENCRYPTION
: 設定為
tls
。
由 Sync 寄送信件
MailService.php 3 3 GitHub Commit : 由 sync 寄送信件
namespace App\Services; use Illuminate\Mail\Message; use Mail; class MailService { public function mailBySync() { Mail::send('welcome', [], function (Message $message) { $message->sender('oomusou@gmail.com'); $message->subject('Laravel 5.2 mail by Sync'); $message->to('oomusou@gmail.com'); }); } }
使用 Mail::send()
寄送信件。
- 第 1 個參數為 blade 檔案名稱。
- 第 2 個參數為要傳給 blade 的陣列,若不傳任何資料,也要傳一個空陣列。
-
第 3 個參數為 closure,主要是 Laravel 希望你將
Message
物件資料填滿。
實際執行會發現, Mail::send()
會需要等 1 到 2 秒才會執行完,因為是同步,要等 smtp server 回應後才會繼續執行。
單元測試 : 由 Queue 寄送信件
MailServiceTest.php 4 4 GitHub Commit : 單元測試 : 由 queue 寄送信件
use App\Services\MailService; class MailServiceTest extends TestCase { /** @test */ public function 由queue寄送信件() { /** arrange */ /** act */ App::call(MailService::class . '@mailByQueue'); /** assert */ $this->assertTrue(true); } }
這裡沒做任何測試,只是透過 PHPUnit 啟動 queue 方式寄送信件。
設定本機環境使用 Queue
.env 5 5 GitHub Commit : 設定本機環境使用 queue
APP_ENV=local APP_DEBUG=true APP_KEY=base64:PABSMLELX37jW2jJdwm9Fk6LvUWupulQXAWDZcfA7xE= APP_URL=http://localhost DB_CONNECTION=sqlite DB_HOST=127.0.0.1 DB_PORT=3306 #DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=file #QUEUE_DRIVER=sync QUEUE_DRIVER=database REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME=[your gmail address] MAIL_PASSWORD=[your password] MAIL_ENCRYPTION=tls
15 行
#QUEUE_DRIVER=sync QUEUE_DRIVER=database
QUEUE_DRIVER
由 sync
改成 database
。
設定測試環境使用 Queue
phpunit.xml 6 6 GitHub Commit : 設定測試環境使用 queue
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="bootstrap/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false"> <testsuites> <testsuite name="Application Test Suite"> <directory suffix="Test.php">./tests</directory> </testsuite> </testsuites> <filter> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">./app</directory> <exclude> <file>./app/Http/routes.php</file> </exclude> </whitelist> </filter> <php> <env name="APP_ENV" value="testing"/> <env name="QUEUE_DRIVER" value="database"/> <env name="DB_CONNECTION" value="sqlite"/> </php> </phpunit>
24 行
<php> <env name="APP_ENV" value="testing"/> <env name="QUEUE_DRIVER" value="database"/> <env name="DB_CONNECTION" value="sqlite"/> </php>
-
將
CACHE_DRIVER
與SESSION_DRIVER
部分刪除。 -
將
QUEUE_DRIVER
改成database
。 -
將
DB_CONNECTION
改成sqlite
。
CACHE_DRIVER
與 SESSION_DRIVER
必須刪除,否則無法再測試環境使用非同步方式寄送 email。
儘管我們之前已經在 .env
的 QUEUE_DRIVER
改成 database
,但 phpunit.xml
的 QUEUE_DRIVER
的 sync
設定會覆蓋掉 .env
的設定,所以這邊也要改成 database
。
不能如一般單元測試的 DB_CONNECTION
設定成 sqlite_testing
,也就是將資料庫設定成 :memory:
,因為這種方式是使用 SQLite in Memory,只要測試一執行完,SQLite in Memory 就會被刪除,因此無法被 Forever 使用,因而無法使用非同步方式寄送 email。
建立 jobs table
oomusou@mac:~/MyProject$ php artisan queue:table oomusou@mac:~/MyProject$ php artisan migrate
目前是將 queue 建立在資料庫,因此須建立 jobs
table。
7
7
GitHub Commit : 建立 jobs table
安裝 Forever
在 Laravel 的 Queues
文件中,是建議大家使用 Supervisor
,它會讓 php artisan queue:listen
在背景執行,持續地消耗 queue,不過 Supervisor
是 Linux 的程式,無法在 Windows 與 macOS 執行,因此對於開發並不方便。
這裡介紹的是由 Node.js 開發的 Forever
,功能與 Supervisor
完全一樣,但因為由 Node.js 所開發,在 Windows、 macOS 與 Linux 都可執行,只要能能成功安裝 Node.js 即可。
oomusou@mac:~/$ npm install -g forever
將 forever
全域安裝。
使用 Forever 啟動 Mail Queue
oomusou@mac:~/$ forever start -l /Users/oomusou/Code/Laravel/Laravel52QueueForever/forever.log -c php artisan queue:listen
- start : 使用 forever 啟動其他服務。
-
-l
: 指定 log 位置,
Forever
預設將 log 放在~/.forever
目錄下,且每次啟動為亂數,若你想將 log 指定在特定目錄,並使用特定檔名,則必須使用-l
,且必須使用完整路徑。 - -c : 要啟動的 CLI 命令。
由 Queue 寄送信件
MailService.php 8 8 GitHub Commit : 由 queue 寄送信件
namespace App\Services; use Illuminate\Mail\Message; use Mail; class MailService { public function mailByQueue() { Mail::queue('welcome', [], function (Message $message) { $message->sender('oomusou@gmail.com'); $message->subject('Laravel 5.2 mail by Queue'); $message->to('oomusou@gmail.com'); }); } }
使用 Mail::queue()
寄送信件,其他參數與 Mail::send()
完全相同。
實際執行發現, Mail::queue()
會馬上執行完,不會有任何等待,因為是非同步,不用等 smtp server 回應就可繼續執行。
Conclusion
-
由同步改成非同步方式寄信,在程式方面,只要將
Mail::send()
改成Mail::queue()
即可。 - Laravel 支援多種 queue,本文使用最簡單的資料庫方式。
-
Forever
為 Node.js 所開發,在 Windows、macOS 與 Linux 都可使用,非常方便。
Sample Code
完整的範例可以在我的 GitHub 上找到。
Reference
Taylor Otwell, Mail
Taylor Otwell, Queues
foreverjs, forever