從零開始的Laravel生活
前言
日前接了學校事務組的打工,有一個要寫迷你系統的需求。為了順便練練 PHP 的功力,這次選用 Laravel 做主要框架,不過做起來比想像中複雜非常多,看起來我太高估自己的學習能力了。
正文
專案架構
一開始原本打算簡單做,所以大概是這樣想的:
東西打算放到學校的虛擬機上面跑,就用 Docker 包了 Container 來讓環境乾淨一點。這裡的圖省略了一些組件,在包 Container 時,為了要讓 Lavarel 有辦法提供 Web 服務,還有一項 NGINX,另外為了讓測試環境更舒適,我也順便把 Adminer 也包進去了,最後變成大禮包(?
開發環境搭建
由於正式上線要使用 Docker,開發環境就順便用 Docker 架了。
身為 Docker 萌新,先上網找了 Laradock,但是看起來真的超肥,決定自己來弄。各式各樣爬文加上拼拼湊湊的結果,好不容易終於把測試環境架起來。
docker-compose.yml
1 | version: "2" |
docker-compose.yml
主要是設定整個架構是怎麼堆起來的。其中的服務名稱和 port 設定會影響到各個服務之間的互連。
Adminer 要連線到 MariaDB 時,host 一定要寫服務的名稱(這裡是
database
)。如果在 Adminer 連線時寫localhost
會變成 Adminer 這個服務自己去連自己的 3306 port,然而 Adminer 那邊 3306 是沒開的,因此要設定成database
讓他自己去抓
app.dockerfile
1 | FROM php:8.0-fpm |
基本上只是把該裝的程式裝一裝,不過當初卡很久,常常環境裡面東缺西缺的,這裡的版本應該算穩定
web.dockerfile
1 | FROM nginx:latest |
只是拿專案資料夾裡面的
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.php
和config/fortify.php
裡面找到。
config/jetstream.php
1 | 'features' => [ |
config/fortify.php
1 | 'features' => [ |
基本上全部都關掉了,正式上線的時候會連底下的註冊都不剩。
最終結果感覺就像是拿現成的網站模板來做後台...
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 | class Donor extends Model |
基本上就是照著官方文檔把該填的東西填上,比較特別的地方是$cast
可以幫忙轉型,而attributes
可以設定初始值。
之後可能會注意到資料庫裡面還多了
created_at
與updated_at
兩個欄位,這是 Laravel 自動加上和維護的。不想要的話也可以參考官方文件把它拿掉或改名。
其實只有 Model 並沒有用,我們還要定義一下 table 的格局,因此要轉至 migration 的部分。
節錄自database/migrations/2021_08_16_130538_create_donors_table.php
1 | class CreateDonorsTable extends Migration |
同樣沒有做太多修改,只是把 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 | class DonorController extends Controller |
做的事情同樣不難,只是把 post 進來的資料各個做檢查,檢查通過以後就建一個新的Donor
物件,把各項屬性放上去以後呼叫save()
讓 Model 幫我們把資料存進 DB 裡。
這裡的 Controller 是作為 ResourceController 使用,所以對應的函式會被套上對應的路徑和 HTTP 方法。以這裡的
store
為例,對應到的路由會是/donors
的 post。官方文件的 ResourceController 有更詳細的說明。
更新
建立完以後也要有辦法更新資料,這次除了要驗證對應的參數以外,還要把原本的資料抓出來。
節錄自app/Http/Controllers/DonorController.php
1 | class DonorController extends Controller |
直接 call model 的update()
並且把資料當成 array 傳進去事情就解決了。
你可能很疑惑為什麼可以直接帶入 Donor 物件,而這就是 Laravel 的魔法!Laravel 的Service Container會幫忙把 Controller 上面需要的參數做注入,所以可以直接操作,我們只要做類型提示,關於這方面更詳細的文章官方文檔有
刪除
利用上面提到的依賴注入直接把 model 抓出來刪掉就好。
節錄自app/Http/Controllers/DonorController.php
1 | class DonorController extends Controller |
設定 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 | import asyncio |
之前在搬樹莓派資料的時候就寫過類似的程式,而且也是運用 async 來加速,所以這次就把上次的 code 拿來用。關於 Config 還有 DBManager 這兩個類別則是從過去寫過的 Discord Bot 搬過來,稍微修修改改再加上 Excel 的讀取,用來匯入資料的腳本就完成了!
寄信功能
終於來到了重頭戲了!原本這個程式的功能就是要做信件的自動通知,後台系統只是順便加上去的。一開始原本打算用 Python 的腳本來實現自動通知,不過看到 Laravel 內建了非常完整的寄信功能實作,所以讓我決定直接採用 Laravel 來串。
指令實作
為了要方便 crontab 執行,把寄信的功能寫成指令應該會是最好的方法。剛好 Laravel 已經提供了一個指令的介面,要做的就只是把指令生出來而已。一如既往執行
php artisan make:command SendEmail
之後就在這裡面寫寄信的實作邏輯。
在此先實作一個寄信的功能,再用另一個指令把最後要的功能串出來會比較靈活而且方便測試。
節錄自app/Console/Commands/SendMail.php
1 | class SendMail extends Command |
先寫這樣,其他像是指令描述之類的可以到官方文檔查。
上面還沒有實現寄信邏輯,只是先把指令建起來。寄信的邏輯會在下面的內容看到。
設定
為了能順利發信,必須先把發信的設定先喬好。Laravel 可以用 Mailgun、Postmark、AWS SES 等服務來做發信,不過這裡是採用上級交代的電算郵件來做功能,所以要用到的就是 smtp。
設定到.env
裡面調整就可以了,不用再去動原始碼。
節錄自.env
1 | ADMIN_MAIL=admin@example.com |
ADMIN_MAIL
是我自己加上去的,之後直接用這個來調整要看到副本的管理員。
關於 HOST 還有 PORT 那些有的沒的參數,根據自己要用到的 Server 設定就可以了。這裡的參數是學校電算給的,在網路上就可以查到。
FROM_ADDRESS 可以跟寄 mail 的是不同帳號,不過對方收到以後可能會被平台當成高機率的詐騙郵件。
學校的 TLS 似乎版本太舊,導致實際上要寄件的時候會被 Open SSL 擋下來。解決方法可以參考這篇文章
建立信件
Laravel 的信件都用 Class 來表示,為了簡化流程直接用
php artisan make:mail ExpireAlert
編輯生成出來的檔案,稍微加點內容就完成了
節錄自app/Mail/ExpireAlert.php
1 | class ExpireAlert extends Mailable |
比較特別的地方是傳資料給信件的部分。Laravel 會自動把 Class 裡面設定成 public 的參數傳給 view,簡化了工作流程,我們只要在__construct()
裡面把初始值設定完即可。
__construct()
其實就是建構子啦!所以 Donor 的參數就從生成物件的時候順便傳進來。
用 markdown 的信件比較好寫,也可以嵌入一些已經寫好的樣板來加快開發速度。不過我在嵌入行內圖片的時候碰到了不小的問題,目前還沒解決,只能把行內圖片放棄。
markdown()
裡面的emails.expire.alert
會被解析成resources/views/emails/expire/alert.blade.php
markdown 信件樣板直接參考官方文擋說明即可,這裡就不多贅述。
寄信
回到原本的寄信指令,這次要把寄信的邏輯也塞進去了,所以插入
1 | Mail::to($donor->email)->bcc(env('ADMIN_MAIL', ''))->send(new ExpireAlert($donor)); |
用Mail::to()
就可以直接發信了,裡面的參數會自動抓name
和email
,這裡是明確的指定 email 進去。後面的bcc()
直接把信件密件副本給管理員。寄件成功後對該筆資料做個紀錄,寄件的功能就寫完了。
完成篩選寄件
建立指令
寄信的指令功能完成了,再來就利用現有的指令把需要的功能串起來。
最終目標是要篩選認養期即將到期的人,並且寄一封信。所以現在寫一個指令來處理篩選的部分:
php artisan make:command CheckExpireDate
節錄自app/Console/Commands/CheckExpireDate.php
1 | class CheckExpireDate extends Command |
篩選內容
弄出了一個指令樣板,接下來處理資料庫過濾的部分。這裡可以用scope query來寫,所以到 Model 裡面去改
節錄自app/Models/Donor.php
1 | class Donor extends Model |
做點簡單的邏輯過濾就好了,function 名稱要記得在前面加 scope 並採用駝峰式命名。
完成指令
過濾邏輯已經寫好了,所以回到指令部分插入下段:
1 | foreach (Donor::noReminded()->expired()->get() as $donor) { |
用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 | GET: http://example.com/events/80 |
Server 接到特殊請求後會回傳一份 json 給瀏覽器,讓瀏覽器去更新頁面
1 | 200 OK |
內容其實就是 component 的資料,藉此達成頁面更新的效果。
version 是 Interia 用來判斷 Server 的前端有沒有檔案更動用的,如果前端檔案被更動就表示頁面一定要強制刷新。如何設定 version 可以參考iT 邦幫忙,寫得很清楚
render 頁面
用法跟 Laravel 原本的 Router 差不多,寫法一樣有很多種,下面的方式都是可以的。
1 | // 一般的路由寫法,closure裡面的東西也可以換成Controller處理 |
如果要傳遞 props 給前端,可以參考下面的寫法:
1 | return Inertia::render('Dashboard', ['donorsData' => Donor::all()]); |
在 array 裡面指定好就會被傳到前端了。要注意的是傳到前端以後使用者是看得到資料的。如果 database 裡面有機敏資料一定要用only()
過濾過,不然就變成大大大放送了。
Error Handleing
在測試環境時,Interia 碰到錯誤或是非 Interia 的 response 會直接在頁面上跳一個 iframe 出來。正式上線的時候必須改掉,就參考了 Interia 的官方文檔加入一些錯誤處理的邏輯。
節錄自app/Exceptions/Handler.php
1 | class Handler extends ExceptionHandler |
這裡檢查了錯誤代碼做對應的響應,把錯誤代碼送到前端給前端的頁面做渲染。
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 或其他方式聯絡我。感謝你的觀看。