從零開始的Laravel生活

前言

日前接了學校事務組的打工,有一個要寫迷你系統的需求。為了順便練練 PHP 的功力,這次選用 Laravel 做主要框架,不過做起來比想像中複雜非常多,看起來我太高估自己的學習能力了。

正文

專案架構

一開始原本打算簡單做,所以大概是這樣想的:

簡易專案架構

東西打算放到學校的虛擬機上面跑,就用 Docker 包了 Container 來讓環境乾淨一點。這裡的圖省略了一些組件,在包 Container 時,為了要讓 Lavarel 有辦法提供 Web 服務,還有一項 NGINX,另外為了讓測試環境更舒適,我也順便把 Adminer 也包進去了,最後變成大禮包(?

開發環境搭建

由於正式上線要使用 Docker,開發環境就順便用 Docker 架了。

身為 Docker 萌新,先上網找了 Laradock,但是看起來真的超肥,決定自己來弄。各式各樣爬文加上拼拼湊湊的結果,好不容易終於把測試環境架起來。

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
version: "2"
services:
app:
build:
context: ./
dockerfile: app.dockerfile
working_dir: /var/www
volumes:
- ./:/var/www
environment:
- "DB_PORT=3306"
web:
build:
context: ./
dockerfile: web.dockerfile
working_dir: /var/www
volumes_from:
- app
ports:
- 8080:80
environment:
- NGINX_PORT=80
database:
image: mariadb:latest
restart: always
volumes:
- dbdata:/var/lib/mysql
environment:
- "MYSQL_DATABASE=tree"
- "MYSQL_USER=tree"
- "MYSQL_PASSWORD=secret"
ports:
- "3306:3306"
adminer:
image: adminer:latest
restart: always
ports:
- 48763:8080
environment:
ADMINER_DEFAULT_SERVER: database
volumes:
dbdata:

docker-compose.yml主要是設定整個架構是怎麼堆起來的。其中的服務名稱和 port 設定會影響到各個服務之間的互連。

Adminer 要連線到 MariaDB 時,host 一定要寫服務的名稱(這裡是database)。如果在 Adminer 連線時寫localhost會變成 Adminer 這個服務自己去連自己的 3306 port,然而 Adminer 那邊 3306 是沒開的,因此要設定成database讓他自己去抓

app.dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM php:8.0-fpm

RUN apt-get update

RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev

RUN apt-get clean;

RUN apt-get install -y \
libzip-dev \
zip \
&& docker-php-ext-install zip pdo pdo_mysql

RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli

RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN apt-get update &&\
apt-get install -y gnupg &&\
curl -sL https://deb.nodesource.com/setup_14.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs &&\
npm install --global gulp-cli

CMD php-fpm

基本上只是把該裝的程式裝一裝,不過當初卡很久,常常環境裡面東缺西缺的,這裡的版本應該算穩定

web.dockerfile

1
2
3
FROM nginx:latest

ADD vhost.conf /etc/nginx/conf.d/default.conf

只是拿專案資料夾裡面的vhost.conf蓋掉預設的設定檔,這樣就不用進到裡面去調。

Laravel

Starter Kits

這次專案打算把 Laravel 當成全端框架,因此要搞定不少問題。一開始就先爬了一堆文來想辦法整合 Laravel 和 Vue。

這次專案不打算把前端 Server 和後端 Server 分開的原因是為了減少 Server 的系統資源消耗。我記得學校虛擬機給的記憶體限制只有 2G 而且還要跟其他服務搶...

最後在翻 Laravel 文檔的時候發現了 Laravel Jetstream 這個 Starter Kits,省下了後續不少開發麻煩,也為 Vue 和 Laravel 的整合提供了一個方案。

這裡安裝的是 Inertia 作為前端的版本。Livewire 版本的前端是使用 blade 模板

Jetstream 到底有多強?

以下是官方文檔給的 Features:

  • Authentication
  • Registration
  • Profile Management
  • Password Update
  • Password Confirmation
  • Two Factor Authentication
  • Browser Sessions
  • API
  • Teams

感覺已經可以搭出一個現代網站了,不過我只是要寫個管理後台系統,所以只需要開幾個功能。關於 Features 的控制可以在config/jetstream.phpconfig/fortify.php裡面找到。

config/jetstream.php

1
2
3
4
5
6
7
'features' => [
// Features::termsAndPrivacyPolicy(),
// Features::profilePhotos(),
// Features::api(),
// Features::teams(['invitations' => true]),
// Features::accountDeletion(),
],

config/fortify.php

1
2
3
4
5
6
7
8
9
10
11
12
'features' => [
Features::registration(),
// Features::resetPasswords(),
// Features::emailVerification(),
// Features::updateProfileInformation(),
// Features::updatePasswords(),
/*
Features::twoFactorAuthentication([
'confirmPassword' => true,
]),
*/
],

基本上全部都關掉了,正式上線的時候會連底下的註冊都不剩。

最終結果感覺就像是拿現成的網站模板來做後台...

Database Model and Migration

由於登入的驗證那些 Jetstream 都幫忙弄好了,我們只要設計用來儲存認養人和認養資料的 Database model,所以直接用 Laravel 給的 artisan 工具來做檔案生成。

在專案位置 cmd 下:

php artisan make:model -a

這個指令會幫忙把 model 的基本架構建起來,除此之外,也會幫忙生成對應的 migration、seed、controller 等。未來這些功能可能都會用到,所以先建起來也無妨。

artisan 是 Laravel 給的命令列程式,可以用來執行內建或是自己擴充的指令。make 功能都是用來建立一些程式樣板的,可以加快開發速度。

migration 可以用來管理資料表的版本,哪天動了 table 的格局以後可以利用 migration 做退版或升級之類的。一開始的 table 生成也要靠他。

東西都建好之後就可以開始建 model 啦!

節錄自app/Models/Donor.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Donor extends Model
{
use HasFactory;

protected $fillable = [
'identity',
'name',
'amount',
'species',
'position',
'start_date',
'expire_date',
'message',
'email',
'has_reminded'
];

protected $casts = [
'has_reminded' => 'boolean',
'start_date' => 'date:Y-m-d',
'expire_date' => 'date:Y-m-d'
];

protected $attributes = [
'has_reminded' => false,
];
}

基本上就是照著官方文檔把該填的東西填上,比較特別的地方是$cast可以幫忙轉型,而attributes可以設定初始值。

之後可能會注意到資料庫裡面還多了created_atupdated_at兩個欄位,這是 Laravel 自動加上和維護的。不想要的話也可以參考官方文件把它拿掉或改名。

其實只有 Model 並沒有用,我們還要定義一下 table 的格局,因此要轉至 migration 的部分。

節錄自database/migrations/2021_08_16_130538_create_donors_table.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CreateDonorsTable extends Migration
{
public function up()
{
Schema::create('donors', function (Blueprint $table) {
$table->id();
$table->string('identity')->nullable();
$table->string('name');
$table->integer('amount');
$table->string('species')->nullable();
$table->string('position')->nullable();
$table->date('start_date')->nullable();
$table->date('expire_date')->nullable();
$table->string('message')->nullable();
$table->string('email')->nullable();
$table->boolean('has_reminded');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('donors');
}
}

同樣沒有做太多修改,只是把 table 的定義弄上去。關於型別還有可以使用的限制一樣可以參考官方文檔


兩項東西都弄好後就可以正式上啦!一樣要動用到 artisan 幫我們完成工作,在 cmd 執行:

php artisan migrate

migrate 指令會去比對 migrate 資料庫中的狀態和資料表有沒有被建立,所以會在 database 裡面再看到一個 migrations 的 table,內容紀錄的都是 migration 的紀錄,如果想觀看的話可以用php artisan migrate:status

執行完畢以後在 migrate 裡面定義好的 table 就會自己出現了。

Model 操作

儲存

先來寫把資料存進 table 的方法,為了方便就直接去 Controller 裡面寫

節錄自app/Http/Controllers/DonorController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DonorController extends Controller
{
public function store(Request $request)
{
$request->validate([
'identity' => 'nullable',
'name' => 'required',
'amount' => 'required|integer|gt:0',
'species' => 'nullable',
'position' => 'nullable',
'start_date' => 'nullable|date_format:Y-m-d',
'expire_date' => 'nullable|date_format:Y-m-d',
'message' => 'nullable',
'email' => 'nullable',
]);

$donor = new Donor;

$donor->identity = $request->identity;
$donor->name = $request->name;
$donor->amount = $request->amount;
$donor->species = $request->species;
$donor->position = $request->position;
$donor->start_date = $request->start_date;
$donor->expire_date = $request->expire_date;
$donor->message = $request->message;
$donor->email = $request->email;

$donor->save();
return redirect()->back();
}
}

做的事情同樣不難,只是把 post 進來的資料各個做檢查,檢查通過以後就建一個新的Donor物件,把各項屬性放上去以後呼叫save()讓 Model 幫我們把資料存進 DB 裡。

這裡的 Controller 是作為 ResourceController 使用,所以對應的函式會被套上對應的路徑和 HTTP 方法。以這裡的store為例,對應到的路由會是/donors的 post。官方文件的 ResourceController 有更詳細的說明。

更新

建立完以後也要有辦法更新資料,這次除了要驗證對應的參數以外,還要把原本的資料抓出來。

節錄自app/Http/Controllers/DonorController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DonorController extends Controller
{
public function update(Request $request, Donor $donor)
{
$request->validate([
'identity' => 'nullable',
'name' => 'required',
'amount' => 'required|integer|gt:0',
'species' => 'nullable',
'position' => 'nullable',
'start_date' => 'nullable|date_format:Y-m-d',
'expire_date' => 'nullable|date_format:Y-m-d',
'message' => 'nullable',
'email' => 'nullable',
]);

if (!is_null($donor)) {
$donor->update($request->only(['identity', 'name', 'amount', 'species', 'position', 'start_date', 'expire_date', 'message', 'email']));
session()->flash('flash.alert', '已更新一筆資料');
session()->flash('flash.alertStyle', 'success');
}
else {
session()->flash('flash.alert', '更新資料失敗');
session()->flash('flash.alertStyle', 'danger');
}
return redirect()->back();
}
}

直接 call model 的update()並且把資料當成 array 傳進去事情就解決了。

你可能很疑惑為什麼可以直接帶入 Donor 物件,而這就是 Laravel 的魔法!Laravel 的Service Container會幫忙把 Controller 上面需要的參數做注入,所以可以直接操作,我們只要做類型提示,關於這方面更詳細的文章官方文檔

刪除

利用上面提到的依賴注入直接把 model 抓出來刪掉就好。

節錄自app/Http/Controllers/DonorController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DonorController extends Controller
{
public function destroy(Donor $donor)
{
if (!is_null($donor)) {
$donor->delete();
session()->flash('flash.alert', '已刪除一筆資料');
session()->flash('flash.alertStyle', 'success');
}
else {
session()->flash('flash.alert', '找不到該項資料');
session()->flash('flash.alertStyle', 'danger');
}
return redirect()->back();
}
}

設定 Resource Router

基本上要設定一個 Resource Router 很簡單,只要用:

1
Route::resource('donors', DonorController::class);

關於路由設定那些 Laravel 會自己去抓,我們把身分驗證那些的 middle ware 設定好就行。

寫到後面我有發現使用 Inertia 的網站不適合使用 Restful API。為了配合 Inertia 的 render 方式不設計 Restful API 反而省下麻煩,不過我都寫下去了就懶得改

如果實在是受不了的話可以用用看Flexible Presenter

匯入資料

對方給了一份 Excel 檔案,不過因為檔案裡面有不少錯誤跟格式不整齊的地方,所以就算寫了腳本來做匯入還是搞了很久。

腳本採用有眾多套件支援而且本身開發速度又極快的 Python,對 Excel 做讀取再塞入 database 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import asyncio

import openpyxl

from api.config.config import Config
from api.mysql.db import DBManager
from api.mysql.tree_db import TreeDB


async def sendData():
workbook = openpyxl.load_workbook('data.xlsx')
sheet = workbook['工作表1']
tasks = []
for y in range(1, 198):
row = list()
row.append(sheet.cell(row=y, column=1).value)
row.append(sheet.cell(row=y, column=2).value)
row.append(sheet.cell(row=y, column=3).value)
row.append(sheet.cell(row=y, column=4).value)
row.append(sheet.cell(row=y, column=5).value)
try:
row.append(sheet.cell(row=y, column=6).value.split('-')[0].replace('/', '-'))
except:
row.append(None)
try:
row.append(sheet.cell(row=y, column=6).value.split('-')[1].replace('/', '-'))
except:
row.append(None)
row.append(sheet.cell(row=y, column=7).value)
row.append(sheet.cell(row=y, column=8).value)
tasks.append(asyncio.create_task(TreeDB.add_donor(row)))
await asyncio.sleep(0.1)
await asyncio.gather(*tasks)
print('完成')


async def main():
print('開始檢查設定檔')
await Config.init()
print('設定檔檢查完畢')
print('開始檢查資料庫系統')
await DBManager.init()
print('資料庫系統檢查完畢')
await sendData()


asyncio.get_event_loop().run_until_complete(main())

之前在搬樹莓派資料的時候就寫過類似的程式,而且也是運用 async 來加速,所以這次就把上次的 code 拿來用。關於 Config 還有 DBManager 這兩個類別則是從過去寫過的 Discord Bot 搬過來,稍微修修改改再加上 Excel 的讀取,用來匯入資料的腳本就完成了!

寄信功能

終於來到了重頭戲了!原本這個程式的功能就是要做信件的自動通知,後台系統只是順便加上去的。一開始原本打算用 Python 的腳本來實現自動通知,不過看到 Laravel 內建了非常完整的寄信功能實作,所以讓我決定直接採用 Laravel 來串。

指令實作

為了要方便 crontab 執行,把寄信的功能寫成指令應該會是最好的方法。剛好 Laravel 已經提供了一個指令的介面,要做的就只是把指令生出來而已。一如既往執行

php artisan make:command SendEmail

之後就在這裡面寫寄信的實作邏輯。

在此先實作一個寄信的功能,再用另一個指令把最後要的功能串出來會比較靈活而且方便測試。

節錄自app/Console/Commands/SendMail.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SendMail extends Command
{
protected $signature = 'mail:send {donor}';

protected $description = '';

public function handle()
{
$donorID = $this->argument('donor');

$donor = Donor::find($donorID);

if (!is_null($donor)) {
}
else {
$this->error('寄件失敗! 找不到該名認養者!');
}
}
}

先寫這樣,其他像是指令描述之類的可以到官方文檔查。

上面還沒有實現寄信邏輯,只是先把指令建起來。寄信的邏輯會在下面的內容看到。

Mail

設定

為了能順利發信,必須先把發信的設定先喬好。Laravel 可以用 Mailgun、Postmark、AWS SES 等服務來做發信,不過這裡是採用上級交代的電算郵件來做功能,所以要用到的就是 smtp。

設定到.env裡面調整就可以了,不用再去動原始碼。

節錄自.env

1
2
3
4
5
6
7
8
9
10
ADMIN_MAIL=admin@example.com

MAIL_MAILER=smtp
MAIL_HOST=smtp.cc.ncu.edu.tw
MAIL_PORT=25
MAIL_USERNAME=username
MAIL_PASSWORD=secret
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=address@example.com
MAIL_FROM_NAME="${APP_NAME}"

ADMIN_MAIL是我自己加上去的,之後直接用這個來調整要看到副本的管理員。

關於 HOST 還有 PORT 那些有的沒的參數,根據自己要用到的 Server 設定就可以了。這裡的參數是學校電算給的,在網路上就可以查到。

FROM_ADDRESS 可以跟寄 mail 的是不同帳號,不過對方收到以後可能會被平台當成高機率的詐騙郵件。

學校的 TLS 似乎版本太舊,導致實際上要寄件的時候會被 Open SSL 擋下來。解決方法可以參考這篇文章

建立信件

Laravel 的信件都用 Class 來表示,為了簡化流程直接用

php artisan make:mail ExpireAlert

編輯生成出來的檔案,稍微加點內容就完成了

節錄自app/Mail/ExpireAlert.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ExpireAlert extends Mailable
{
use Queueable, SerializesModels;

public $donor;

public function __construct(Donor $donorData)
{
$this->donor = $donorData;
}

public function build()
{
return $this->subject('mail subject')->markdown('emails.expire.alert');
}
}

比較特別的地方是傳資料給信件的部分。Laravel 會自動把 Class 裡面設定成 public 的參數傳給 view,簡化了工作流程,我們只要在__construct()裡面把初始值設定完即可。

__construct()其實就是建構子啦!所以 Donor 的參數就從生成物件的時候順便傳進來。

用 markdown 的信件比較好寫,也可以嵌入一些已經寫好的樣板來加快開發速度。不過我在嵌入行內圖片的時候碰到了不小的問題,目前還沒解決,只能把行內圖片放棄。

markdown()裡面的emails.expire.alert會被解析成resources/views/emails/expire/alert.blade.php

markdown 信件樣板直接參考官方文擋說明即可,這裡就不多贅述。

寄信

回到原本的寄信指令,這次要把寄信的邏輯也塞進去了,所以插入

1
2
3
4
Mail::to($donor->email)->bcc(env('ADMIN_MAIL', ''))->send(new ExpireAlert($donor));
$this->info('已將信件寄往'.$donor->email.'同時副本給'.env('ADMIN_MAIL', ''));
$donor->has_reminded = true;
$donor->save();

Mail::to()就可以直接發信了,裡面的參數會自動抓nameemail,這裡是明確的指定 email 進去。後面的bcc()直接把信件密件副本給管理員。寄件成功後對該筆資料做個紀錄,寄件的功能就寫完了。

完成篩選寄件

建立指令

寄信的指令功能完成了,再來就利用現有的指令把需要的功能串起來。

最終目標是要篩選認養期即將到期的人,並且寄一封信。所以現在寫一個指令來處理篩選的部分:

php artisan make:command CheckExpireDate

節錄自app/Console/Commands/CheckExpireDate.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CheckExpireDate extends Command
{
protected $signature = 'mail:check';

public function __construct()
{
parent::__construct();
}

public function handle()
{
// do something
$this->info('完成!');
}
}
篩選內容

弄出了一個指令樣板,接下來處理資料庫過濾的部分。這裡可以用scope query來寫,所以到 Model 裡面去改

節錄自app/Models/Donor.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Donor extends Model
{
public function scopeNoReminded(Builder $query)
{
return $query->where('has_reminded', false);
}

public function scopeExpired(Builder $query)
{
$today = date("Y-m-d");
$alertDay = date("Y-m-d", strtotime($today."+2 week"));
return $query->where('expire_date', '>', $today)->where('expire_date', '<', $alertDay);
}
}

做點簡單的邏輯過濾就好了,function 名稱要記得在前面加 scope 並採用駝峰式命名。

完成指令

過濾邏輯已經寫好了,所以回到指令部分插入下段:

1
2
3
4
5
foreach (Donor::noReminded()->expired()->get() as $donor) {
$this->call('mail:send', [
'user' => $donor->id
]);
}

call()來呼叫其他指令,並且把搜到的 Donor id 帶入,讓已經寫好的指令幫忙完成其他的工作。

功能到此全部完成,之後到生產環境上面用 crontab 設定每天執行就好。

Inertia.js

Inertia.js 是用來接 Vue 和 Laravel 的中間層,有一套特別的協定,可以讓 Server 的路由在定向的時候保持有如 SPA(Single Page Application)的體驗。

Inertia 協定

當瀏覽器進到 Inertia 的頁面時,會先拿到一份 HTML 檔還有資源,之後瀏覽器就會把 Inertia 的頁面 render 出來(跟一般的 Vue 頁面差不多)。

在頁面上跳轉時,會需要用到一個 Inertia 的Link component。下面是範例

<Link :href="/somewhere">Let's go</Link>

在頁面上點這個連結以後,Interia 會讓瀏覽器送出一個 XHR 請求,上面附上 Interia 的特殊 Header:

節錄自官方文檔

1
2
3
4
5
GET: http://example.com/events/80
Accept: text/html, application/xhtml+xml
X-Requested-With: XMLHttpRequest
X-Inertia: true
X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5

Server 接到特殊請求後會回傳一份 json 給瀏覽器,讓瀏覽器去更新頁面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
X-Inertia: true
{
"component": "Event",
"props": {
"event": {
"id": 80,
"title": "Birthday party",
"start_date": "2019-06-02",
"description": "Come out and celebrate Jonathan's 36th birthday party!"
}
},
"url": "/events/80",
"version": "c32b8e4965f418ad16eaebba1d4e960f"
}

內容其實就是 component 的資料,藉此達成頁面更新的效果。

version 是 Interia 用來判斷 Server 的前端有沒有檔案更動用的,如果前端檔案被更動就表示頁面一定要強制刷新。如何設定 version 可以參考iT 邦幫忙,寫得很清楚

render 頁面

用法跟 Laravel 原本的 Router 差不多,寫法一樣有很多種,下面的方式都是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一般的路由寫法,closure裡面的東西也可以換成Controller處理
Route::get('/', function () {
return Inertia::render('vuepage');
});

// closure改寫成arrow function
Route::get('/', fn () => Inertia::render('vuepage'));

// Inertia::render簡寫成inertia
Route::get('/', fn () => inertia('vuepage'));

// 連get都換掉變成inertia
Route::inertia('/', 'vuepage');

如果要傳遞 props 給前端,可以參考下面的寫法:

1
return Inertia::render('Dashboard', ['donorsData' => Donor::all()]);

在 array 裡面指定好就會被傳到前端了。要注意的是傳到前端以後使用者是看得到資料的。如果 database 裡面有機敏資料一定要用only()過濾過,不然就變成大大大放送了。

Error Handleing

在測試環境時,Interia 碰到錯誤或是非 Interia 的 response 會直接在頁面上跳一個 iframe 出來。正式上線的時候必須改掉,就參考了 Interia 的官方文檔加入一些錯誤處理的邏輯。

節錄自app/Exceptions/Handler.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Handler extends ExceptionHandler
{
public function render($request, Throwable $e)
{
$response = parent::render($request, $e);

if (!app()->environment(['local', 'testing']) && in_array($response->status(), [500, 503, 404, 403])) {
return Inertia::render('Error', ['status' => $response->status()])
->toResponse($request)
->setStatusCode($response->status());
} else if ($response->status() === 419) {
session()->flash('flash.alert', '頁面已過期,請刷新頁面後重試');
session()->flash('flash.alertStyle', 'danger');
return back();
}

return $response;
}
}

這裡檢查了錯誤代碼做對應的響應,把錯誤代碼送到前端給前端的頁面做渲染。

419 的處理比較特別,是利用 session 放一個提示訊息的資料。back 後使用者會在頁面上看到渲染出來的錯誤訊息(可參考 Jetstream Banner 的實作方法)。

error handle 有很多種處理方法,除了檔案裡面一開始有的register(),這裡的render()也是其中一種。各項差別官方文檔有記載

弄好程式部分就可以把.env裡面的APP_ENV換成production測試看看。

錯誤提示頁面

修改前端後記得執行 mix,如果懶得一直打的話可以用npm run watch來自動監測程式碼更動。若是沒有追蹤成功的話可以參考下面踩坑實錄的部分。

踩坑實錄

npm run watch 無效?

不知道是不是因為用 Docker 的關係,npm run watch沒辦法偵測到檔案變動,所以改用npm run watch-poll來解決問題。

npm run watch-poll會以設定好的時間定期的去檢查檔案更動,只要檔案有變動就幫忙 mix 一次。靠著這個指令可以解決npm run watch監測不到的問題。

郵件 inline 圖片嵌入失敗

網路上不少人都有碰到圖片不能嵌入的問題,目前我也還沒找到解法。最接近的解應該是用 base64 把<img />插在信件裡面,但是收郵件的平台還是會因為安全性問題把圖片的src吃掉。郵件附件再引入的方法可能可行,不過這部分我還要再研究一下。

最好的方法就是不要放圖片

結語

花了很久終於把這篇超長文章打完。由於程式部署不會這麼快,所以部署篇之後再分一篇文章出來。這篇文章就到此為止,有錯誤的話請用 GitHub 或其他方式聯絡我。感謝你的觀看。