(如果你很著急,可以直接跳到 解決辦法 ~)
我們都知道 Laravel 是迄今為止最受歡迎的 PHP 框架。 它的目錄結構好、組織有序、定義簡單。當我們在一箇中小型專案工作時,使用 Laravel 提供目錄結構是非常友好的。 但是,當它開始成為一個超過 50 個模型的大型應用程式時,我們就已經是一腳踩進自己埋的坑裡面了。且再難回頭!
維護一個大的應用程式真的不是開玩笑的,它是需要好好地被思考和設計。而 Laravel 的預設的目錄結構對於這種情況明顯是心有餘而力不足的。
首先,我們可以來看看 Laravel 預設的目錄成為大型應用程式產生的變化。
Laravel 預設的目錄結構就像這樣:
|- app/
|- Console/
|- Commands/
|- Events/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Jobs/
|- Listeners/
|- Providers/
|- User.php
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
這樣的目錄結構設計沒有任何問題。 而當我們的業務邏輯稍微複雜一點時,我們通常會用 Repositories、Transformers 等這些個資料夾來劃分。 跟下面這個差不多:
|- app/
|- Console/
|- Commands/
|- Events/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Jobs/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
這顯然是一個目錄結構設計友好的 Laravel 專案。 看看 Models 資料夾裡面:
|- app/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Merchant.php
|- Store.php
|- Product.php
|- Category.php
|- Tag.php
|- Client.php
|- Delivery.php
|- Invoice.php
|- Wallet.php
|- Payment.php
|- Report.php
看上去也不是那麼糟糕對吧!這裡面也建立了一個資料夾 Services 專門處理所有的業務邏輯。還有 Repositories、Transformers、Validators 這些有著差不多相同的類的資料夾。很多人也覺得這樣的設計很不錯,並且樂於這樣去設計。不過完成僅僅只是單個實體/模型的工作需要瀏覽不同的資料夾和檔案,即操作很多類,寫各種介面,光是這一點讓一些開發者覺得很麻煩。
但問題的關鍵不在於用不同的資料夾去劃分,而是開發者維護程式碼和服務之間的通訊。
分析下前面的程式碼結構可以看到:
- 這是一個 龐大 的應用程式
- 對於一些開發人員來說,很難 維護
- 生產力 低(在考慮系統內部連線上需要耗費時間)
- 程式碼的 規模調整 也是一個問題
解決方案是顯而易見的 —— 微服務。即使我們使用 SOA(面向服務架構),我們還是需要將我們的龐大的應用分解成較小的獨立的部分,以便日後將其分開擴充套件。照理來說這個解決方法挺好的。但現實中我們並沒有這麼做。因為對於將程式碼分解成更小的部分這件事情,說的通常比做的要容易得多得多~
通常分離服務需要兩個簡單的步驟:
- 將子服務(Models、Repositories、Transformers等)移動到新的 PHP 微服務應用程式中
- 重新確認服務函式呼叫的目標確確實實指向到新的微服務中(例如,建立 HTTP 請求)
然後你需要查詢與該服務相關的所有檔案。你可能會很驚奇地發現,我們可能有部分程式碼不經過服務而直接使用了它的模型或儲存庫(我真的幹過這種事)。而這還不是唯一的問題,我們甚至可以總結一下:
- 有太多的檔案要考慮
- 犯錯誤的機會很高
- 容易讓開發者沮喪
- 有時需要重新考慮域名之間的邏輯
- 新的開發者?還是洗洗睡吧!
最後一個原因非常重要,因為新的開發人員很難在短時間內掌握整個應用程式。而通常專案經理不會給他太多的時間去研究。這容易產生給內建物件擴充套件方法、程式碼放在錯誤的位置、甚至會讓下一個新的開發人員更加困惑等問題。
幸運的是,我們已經有一個解決方案 —— HMVC。即將整個應用程式分成較小的部分,每個部分都有自己的檔案和資料夾,如 app/
資料夾,並通過 composer.json 自動載入,如下所示:
|- auth/
|- Exceptions/
|- Http/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- merchant/
|- Console/
|- Events/
|- Exceptions/
|- Http/
|- Jobs/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
但當我們想要將特定模組移動到微服務中時,HMVC 讓事情變得複雜起來,因為我們還是需要在主要程式碼庫中保留控制器、中介軟體等。大多數時候,將程式碼移動到微服務會需要重新定義路由和控制器。這裡面有太多不必要的工作,除開我很懶這個理由,正常的開發者都只想分開那些不得不分開的東西。因此我不是很推崇這種目錄結構。
領域驅動設計也可以是一個解決方案
沒有完美的解決方案,凡事都有兩面性,還是要看每個人偏好。我們不會在這裡討論領域驅動設計(DDD),但總的來說,DDD (可能)將你的 Laravel 應用程式構建為4部分:
- 應用程式(Application) —— 掌管 控制器、中介軟體、路由
- 領域(Domain)—— 掌管業務邏輯 Model、Repository、Transform、Policy 等
- 基礎設施(Infrastructure) —— 掌管 Logging、Email 等常見服務
- 介面(Interface) —— 掌管 View 、lang、assets
這看起來很容易,那麼為什麼我們不這樣構建我們的應用程式,並使用名稱空間?
|- app/
|- Http/ (Application)
|- Controllers/
|- Middleware/
|- Domain/
|- Models/
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Services/
|- Infrastructure/
|- Console/
|- Exceptions/
|- Providers/
|- Events/
|- Jobs/
|- Listeners/
|- resources/ (Interface)
|- assets/
|- lang/
|- views/
|- routes/
|- api.php
|- web.php
因為將專案分割成資料夾是不行的。 這僅僅只是意味著我們只新增了一個父名稱空間。
真理的時刻
你能看到這裡也是挺了不起的了,畢竟好像哪個方法都行不通。我們還是來好好聊一聊真的解決方案吧!看看下面的目錄結構:
|- app/
|- Http/
|- Controllers/
|- Middleware/
|- Providers/
|- Account/
|- Console/
|- Exceptions/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
|- Merchant/
|- Payment/
|- Invoice/
|- resources/
|- routes/
Auth.php
and Acl.php
是 app/Account/
資料夾中的服務檔案。 控制器只能訪問這兩個類並呼叫它們的方法。 其他類永遠不會知道 app/Account/
資料夾中其他剩下的類。 這些服務中的方法將僅接收基本的 PHP 資料型別,例如 array、string、int、bool 和 POPO(Plain Old PHP Object),但沒有類例項。 示例 :
...
public function register(array $attr) {
...
}
public function login(array $credentials) {
...
}
public function logout() {
...
}
...
這裡要注意一個地方,register 函式接收一個陣列屬性的引數,而不是 User 物件。 這個點很重要,因為讓其他類去呼叫該函式的時,不應該讓這個函式知道 User 模型的存在。這是這整個結構你必須要遵守的基本規則。
當我們想分開程式碼
我們的應用程式變得越來越大時,我們希望將 Account
相關的內容分開到單獨的微服務中並將其轉換為 OAuth
伺服器。
所以,我們只是需要移動以下部分
|- Account/
|- Console/
|- Exceptions/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
如果是將應用程式搬到 Lumen:
|- app/
|- Http/
|- Controllers/
| - Middleware/
|- Account/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
|- routes/
|- resources/
不可避免地我們必須在控制器和路由中編寫程式碼,因為我們需要使其成為一個 OAuth
伺服器。
那麼接下來我們需要在主要程式碼庫中做什麼改變?
我們只需要保留服務檔案 Auth.php
和 Acl.php
,並將其方法中的程式碼更改為針對新建立的微服務的 HTTP 請求(或其他資訊傳遞方式)。
...
public function login(array $credentials) {
// change the code here
}
...
整個應用程式將保持不變。 而應用程式的目錄結構將如下所示:
|- app/
|- Console/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Providers/
|- Account/
|- Auth.php
|- Acl.php
|- Merchant/
|- Payment/
|- Invoice/
|- resources/
|- routes/
這個方法只做了很少的事情,就將部分程式碼移動到完全獨立的微服務中。(反正我暫時是想不到更好的方法了)
權衡
正如上面所說的,任何事情都有一個權衡點,對於這個解決方案來說也一樣。而且,這裡我們有個關於 遷移 的問題!因為在上述資料夾結構(分離之前)所有的遷移檔案都是在 database/migrations/
目錄中。但是,當我們要分離一個域時,我們需要確定並移動該域的遷移。這件事情有點難辦,因為我們沒有明確指出哪個遷移屬於哪個域。我們可能需要研究如何將識別符號放在遷移檔案中。
該識別符號可以是域字首。例如,我們可以命名遷移檔案 xxxxxxxxx_create_account_users_table.php
而不是 xxxxxxxxx_create_users_table.php
。如果我們想要,我們也可以使用 account_users
表名替換 user
。我更喜歡在分離過程中識別哪些表要移動。分離遷移檔案可能有點令人沮喪,但是如果我們使用字首或任何型別的標記,那麼這整個過程肯定會變得不那麼痛苦。
我還在計劃嘗試構建一個 Laravel 軟體包的結構,提供 artisan 命令來自動執行檔案生成和分離過程。完成後,我會新增包連結分享出來。
在此之前,如果有更好的意見請分享,我需要你的幫助才能找到最佳解決方案。
週五了,週末愉快:beers:
本文翻譯改編自 Tawsif Aqib 的 Large Scale Laravel Application
本作品採用《CC 協議》,轉載必須註明作者和本文連結