Laravel 項目開發規範
1. 建立開發規範之目的
對於框架設計而言,靈活是件好事,能提供給開發者不同的選項,能讓框架適用更多的場景。
但對於團隊開發來説,大部分時候,更多的選項反而是累贅。因為每個人都可能寫出不一樣的代碼,這無疑增加了項目維護的難度,影響效率。如果是在一箇中大型的商業項目開發中,團隊中有着幾個甚至十幾個開發者,沒有規範的情況下,開發者會根據各自的喜好去選擇,有時甚至出現一個開發者嘗試多個選項的可能,就會造成整個團隊產出的代碼可讀性極低,代碼結構混亂,也為後面的項目代碼的維護帶來了難度。
建立開發規範的目的在於通過統一的代碼風格,保證代碼的易讀性、可維護性。開發規範一旦統一,所有團隊成員嚴格遵守,你會發現,其他成員寫的代碼就如你自己寫的一樣,編碼愉悦感提高了,整個項目代碼閲讀起來更加流暢,工作效率自然也會因此提高,同時代碼的健壯性也得到了保障。
2. 必須遵循的開發原則
- DRY ———— 「Don't Repeat Yourself」, 不寫重複的邏輯代碼。
- KISS ———— 「Keep it Simple, Stupid」, 提倡簡單易讀的代碼,不寫高深、晦澀難懂的代碼,不過度設計。
- 約定大於配置 ———— 「Convention Over Configuration」,優選選擇框架及社區提倡的做法,不過度配置。
- Restful ———— 利用「資源化概念」和標準的 HTTP 動詞來組織程序。
- MVC - Model, View, Controller ,以 MVC 為核心,嚴格控制 Controller 的可讀性和代碼行數。
除了這些原則外,還有不要強制,但非常有用的設計原則,如 SOLID 設計原則(S:單一職責原則、O:開/閉原則、L:里氏替換原則、I:接口隔離原則、D:依賴倒置原則)、組合優於繼承等。
3. 項目目錄結構規範
laravel-app/
├── app/
│ ├── Console/ # Artisan 命令
│ │ ├── Development/ # 存放開發專用命令
│ │ ├── LongPulling/ # 存放死循環執行的命令(可選)
│ │ ├── OneTime/ # 存放一次性命令
│ │ ├── Schedule/ # 存放計劃任務
│ │ └── . # 根目錄存放一般命令(在非常複雜的項目中,應該再按照業務邏輯進行分組)
│ ├── Exceptions/ # 異常處理
│ ├── Http/
│ │ ├── Controllers/ # 控制器
│ │ ├── Middleware/ # 中間件
│ │ └── Requests/ # 表單請求驗證
│ ├── Models/ # Eloquent 模型
│ │ ├── Auth/ # 示例:存放用户、角色、權限相關的模型
│ │ ├── Order/ # 示例:存放訂單相關的模型
│ │ └── Payment/ # 示例:存放支付渠道相關的的模型
│ ├── Providers/ # 服務提供者
│ └── Services/ # 業務邏輯處理
│ ├── Auth/ # 示例:存放登錄、授權相關的業務邏輯文件
│ └── Payment/ # 示例:存放支付相關的業務邏輯文件
├── bootstrap/ # 應用啓動腳本
├── config/ # 配置文件
├── database/
│ ├── migrations/ # 數據庫遷移
│ │ ├── Auth/ # 示例:存放用户、角色、權限相關的遷移文件
│ │ └── Payment/ # 示例:存放支付方式、訂單相關的遷移文件
│ └── seeders/ # 數據填充
├── public/ # 公開靜態文件
├── resources/
│ ├── views/ # Blade 視圖
│ │ ├── layouts/ # 示例:頁面佈局的視圖文件
│ │ ├── common/ # 示例:存放頁面通用元素的視圖文件
│ │ ├── pages/ # 示例:簡單的頁面存放文件夾,如:about、contact 等的視圖文件
│ │ └── resources/ # 示例:對應 Restful 路由的資源路徑名稱,以 URI photos/create 為例,對應 create.blade.php 文件,存放在文件夾 photos 下。
│ │ ├── Auth/ # 示例:存放用户、角色、權限相關的視圖文件
│ │ └── Payment/ # 示例:存放支付方式、訂單相關的視圖文件
│ └── lang/ # 多語言文件
├── routes/ # 路由定義
├── storage/ # 應用存儲(日誌、緩存)
└── tests/ # 測試代碼
├── Feature/ # 示例:存放功能測試、集成測試的測試代碼
└── Unit/ # 示例:存放單元測試代碼,
├── API/ # 示例:存放 API 接口的測試代碼
├── Web/ # 示例:存放 web 接口的測試代碼
├── Admin/ # 示例:存放管理後台接口的測試代碼
├── Command/ # 示例:存放命令行接口的測試代碼
├── Job/ # 示例:存放任務接口的測試代碼
├── Fixtures/ # 示例:測試中使用到樣例數據,必須使用子目錄存儲,絕不直接放置於此目錄下
├── Job/ # 示例:存放任務接口的測試代碼
└── Fixtures/ # 示例:測試中使用到樣例數據,必須使用子目錄存儲,
4. 代碼規範
4.1 代碼風格
代碼風格建議遵循 PSR-12 規範。
PHPStorm 安裝代碼風格檢測插件 php-cs-fixer,安裝教程。
4.1.1 變量
-
使用有意義且可讀的變量名,變量名遵循駝峯命名規範。
// 壞代碼示例 $date = date('y-m-d'); // 好代碼示例 $currentDate = date('y-m-d'); // 從變量名可知,$date 是一個日期,但不知是當天日期還是某個特定時間的日期,相比於 $date,$currentDate更能體現其具體使用場景。 -
不要添加不需要的上下文。如果你的 類或對象 已經有明確的含義,不要在變量中再次重複它。
// 壞代碼 class Car { public $carMake; public $carModel; public $carColor; //... } // 好代碼 class Car { public $make; public $model; public $color; //... }
4.1.2 分支判斷
-
避免嵌套太深(最多不超過三層)並儘早返回。
# 壞代碼示例 function fibonacci(int $n) { if ($n < 50) { if ($n !== 0) { if ($n !== 1) { return fibonacci($n - 1) + fibonacci($n - 2); } return 1; } return 0; } return 'Not supported'; } # 好代碼示例 function fibonacci(int $n): int { if ($n === 0 || $n === 1) { return $n; } if ($n >= 50) { throw new Exception('Not supported'); } return fibonacci($n - 1) + fibonacci($n - 2); }
4.1.3 函數
-
函數名應該語義化。
// 壞代碼示例 class Email { //... public function handle(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 這是什麼? 消息的句柄? 我們現在正在寫入文件嗎? $message->handle(); // 好代碼示例 class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 清晰明瞭 $message->send(); -
函數參數最多不應超過3個。
- 一旦一個方法擁有三個以上的參數將會導致組合爆炸,在進行測試時需要使用每個單獨的參數測試大量不同的情況。
- 零參數是理想的情況。 一兩個參數是可以的,三個應該避免。除此之外的任何東西都應該合併
- 通常,如果您有兩個以上參數這説明你的函數做的事情太多了。如果不是,大多數情況下一個更高級別的對象就足以作為一個參數。
-
一個函數應該只做一件事,同時不要使用標誌位作為函數參數。
// 壞代碼,函數做了兩件事,創建臨時文件或者創建正式文件 function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/' . $name); } else { touch($name); } } // 好代碼:應該基於職責做拆分,拆分為兩個函數 function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/' . $name); } -
避免副作用。避免副作用意味着,儘量不要再函數內使用全局變量或者使用引用傳遞的參數。
// 壞代碼示例 // 通過以下函數引用的全局變量。 // 如果我們有另外一個使用這個名字函數,現在它將是一個數組,它可能會破壞它。 $name = 'Ryan McDermott'; function splitIntoFirstAndLastName(): void { global $name; $name = explode(' ', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];// 好代碼示例 function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott']; -
封裝條件,且函數命名時避免使用否定條件。
// 壞代碼示例 if ($article->state === 'published') { // ... }// 好代碼示例 if ($article->isPublished()) { // ... } private function isPublished() { return $article->state === 'published'; }// 壞代碼示例 function isDOMNodeNotPresent(DOMNode $node): bool { // ... } if (! isDOMNodeNotPresent($node)) { // ... }// 好代碼示例 function isDOMNodePresent(DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }
4.1.4 類與對象
- public 方法和屬性對於更改是最危險的,因為一些外部代碼可能很容易依賴它們,而您無法控制哪些代碼依賴它們。 類中的修改對類的所有用户都是危險的。
- protected 修飾符與 public 一樣危險,因為它們在任何子類的範圍內都可用。 這實際上意味着 public 和 protected 之間的區別僅在於訪問機制,但封裝保證保持不變。類中的修改對所有後代類都是危險的。
- private 修飾符保證代碼 僅在單個類的邊界內修改是危險的(您可以安全地修改並且不會有 疊疊樂效應).
4.2 配置信息與環境變量
- 因 .env 不會被納入版本控制器中,所以本地 .env 文件裏添加變量時必須同步到 .env.example 文件中,以免影響其他項目參與者的工作。
-
所有程序配置信息必須通過 config() 來讀取,所有的 .env 配置信息必須通過 env() 來讀取,不在配置文件以外的範圍使用 env()。原因在於:
- 定義分明,config() 是配置信息,env() 只是用來區分不同環境。
- 統一放置於 config 中還可以利用框架的配置信息緩存功能(php artisan config:cahce)來提高運行效率,當配置信息被緩存後,.env 文件將不會被加載,所以env() 函數將讀取不到 .env 文件中的內容。
- 代碼健壯性, config() 在 env() 之上多出來一個抽象層,會使代碼更加健壯,更加靈活。
4.3 路由
- 不在路由定義文件中書寫「閉包路由」或者其他業務邏輯代碼,因為路由緩存 並不會作用在基於閉包的路由。
- 路由定義文件中要保持乾淨整潔,絕不放置除路由配置以外的其他程序邏輯。
-
路由定義的方式存在多種,推薦使用控制器路由的 Laravel 8+ 推薦寫法(使用命名空間或自動導入),推薦使用下面示例代碼的第二種寫法或第三種寫法。
// 第一種寫法:指向控制器方法 Route::get('/user', 'UserController@index')->name('user.index'); // 第二種寫法:Laravel 8+ 推薦寫法(使用命名空間或自動導入) use App\Http\Controllers\UserController; Route::get('/user', [UserController::class, 'index'])->name('user.index'); // 第三種寫法:基於第二種寫法,並且同時有多個路由用到相同的 Controller use App\Http\Controllers\OrderController; Route::controller(OrderController::class)->group(function () { Route::get('/orders/{id}', 'show'); Route::post('/orders', 'store'); }); -
優先使用 Restful 路由,配合資源控制器路由一起使用,Restful 路由的 uri 應該使用複數形式,例如 uri 應該聲明為 /photos/{photo},而不是 /photo/{photo}。
例如聲明以下的一個資源路由,將會註冊如下表格所示的路由。
use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class);HTTP請求方式 URI 操作 路由名稱 GET /photos index photos.index GET /photos/create create photos.creat POST /photos store photos.store GET /photos/{photo} show photos.show GET /photos/{photo}/edit edit photos.edit PUT/PATCH /photos/{photo} update photos.update DELETE /photos/{photo} destroy photos.destroy 不難想到,有時可能需要定義一個嵌套的資源型路由。例如,照片資源可能被添加了多個評論。那麼可以在路由中使用 . 符號來聲明資源型控制器。這將註冊下表格所示的路由。
use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class);HTTP請求方式 URI 操作 路由名稱 GET /photos/{photo}/comments index photos.comments.index GET /photos/{photo}/comments/create create photos.comments.creat POST /photos/{photo}/comments store photos.comments.store GET /photos/{photo}/comments/{comment} show photos.comments.show GET /photos/{photo}/comments/{comment}/edit edit photos.comments.edit PUT/PATCH /photos/{photo}/comments/{comment} update photos.comments.update DELETE /photos/{photo}/comments/{comment} destroy photos.comments.destroy 注意:如果您需要向資源控制器添加超出默認資源路由集的其他路由,則應在調用 Route::resource 方法之前定義這些路由;否則,由 resource 方法定義的路由可能會無意中優先於您的補充路由。
4.4 控制器
- 優先使用Restful 資源控制器。
-
控制器命名規範,必須使用資源的複數形式,如:
- 類名:PhotosController
- 文件名:PhotosController.php
- 在控制器內不能聲明私有方法,控制器內所有的方法都是外部可訪問的表示路由動作的公有方法,並且控制器裏的所有方法,都應該被使用到,否則應該刪除,也絕對不在控制器裏批量註釋掉代碼,無用的邏輯代碼就必須清除掉。。
- 控制器內不應包含業務邏輯,業務邏輯的實現應該在業務邏輯層去實現。
- 不應該為「方法」書寫很明顯的註釋,這要求方法取名要足夠合理,不需要過多註釋。應該為一些複雜的邏輯代碼塊書寫註釋,主要介紹產品邏輯 - 為什麼要這麼做。
-
表單數據校驗應該放到控制器層(如果是非複雜表單的字段檢驗,可直接在方法內進行校驗,否則需單獨寫一個表單驗證類),驗證通過後再將請求轉發給業務邏輯層。
/** * 存儲一篇新的博客文章。 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]); // 博客文章驗證通過... }
4.5 表單驗證
- 使用表單請求 - FormRequest 類 來處理控制器裏的表單驗證。
-
驗證類命名規則,FormRequest表驗證類命名必須按照控制器方法來命名,且必須添加模型的前綴,類名稱的 Request 後綴也是必須的,這方便了編輯器開始打開文件。如:
- UserCreateRequest
- UserUpdateRequest
4.6 業務邏輯層(Service 層或 Logic 層)
- Controller 層只接收和轉發請求,Model 層只負責模型定義以及數據關聯邏輯。由業務邏輯層負責處理業務邏輯。
- Service 中的方法命名參考 Laravel Model 中的方法命名。
-
所有的 Service 類都必須存放於 app/Services 目錄中,且應避免直接將 Service 類放置於 app/Services 目錄下,應該考慮通過業務邏輯,將其歸類於子目錄中。
Auth —— 存放登錄、授權相關的 Service Payment —— 存放支付相關的 Service Book —— 存放課程相關的 Service - Service 類必須是無狀態的,無狀態意味着無論是控制器方法中、命令行、單元測試中,都可調用。
4.7 模型
- 所有的數據模型文件,都必須存放在:app/Models/ 文件夾中。且所有的 Eloquent 數據模型都必須繼承統一的基類 App\Models\Model,此基類存放位置為 /app/Models/Model.php。
-
命名規範
- 數據模型類名必須為「單數」,如:App\Models\Photo
- 類文件名必須為「單數」,如:App\Models\Photo.php
- 數據庫表名字必須為「複數」,如:photos,users...
- 數據庫表遷移名字必須為「複數」,如:2014_08_08_234417_create_photos_table.php
- 數據填充文件名必須為「複數」,如:PhotosTableSeeder.php
- 數據庫字段名必須是遵循蛇形命名法,如:view_count, is_vip
- 數據庫表主鍵名必須為id
- 數據庫表外鍵必須為「resource_id」,如:user_id, post_id
- 目錄分層:為模型文件按業務邏輯做分層。
- 儘量避免使用 Laravel 的 模型事件。使用模型事件的問題在於,其職能很難界定,所有的業務邏輯都能寫到模型事件中。
4.8 視圖
- 避免在 resources/views 目錄下直接放置視圖文件。頁面佈局文件必須放在 resources/views/layouts 目錄下,頁面組件必須放在 resources/views/common 目錄下,簡單頁面(如404頁面)放在 resources/views/pages 目錄下。對應 Restful 路由的資源路徑名稱,例如以URI photos/create 為例,對應的 create.blade.php 文件,應存放在 resources/views/photos 目錄下。
- 局部視圖文件必須使用 <kdb>_</kdb> 前綴來命名,如:photos/_upload_form.blade.php
-
為了和 Restful 路由器和資源控制器保持一致,視圖命名也必須使用資源視圖的命名方式。例如, 一個完整的 photos 資源對應的視圖文件為以下:
├── photos │ ├── _form.blade.php │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php
5. Git 工作流規範
6. 服務器部署規範
6.1 Composer 依賴安裝規範
-
生產環境或預發佈環境進行項目部署時,必須使用 --no-dev 來安裝項目運行必需依賴。如安裝 predis 擴展:
composer require predis/predis --no-dev -
本地環境或測試環境安裝開發專用擴展時,必須使用 --dev 來安裝開發依賴。如在本地安裝 phpunit 擴展:
composer require phpunit/phpunit --dev
6.2 Composer 依賴版本更新
-
絕對不能在生產服務器執行,composer udpate,原因如下:
- composer udpate 會根據 composer.json 文件的版本約束拉取最新兼容版本,可能導致依賴包升級到未測試的版本,引入不兼容的變更或bug。同時,這將導致開發環境與生產環境的依賴版本可能因此不一致,當系統出現問題時,排查問題變得困難。
- 由於依賴包的升級可能導致項目運行報錯。
- 執行composer udpate後,由於依賴已全局更新,回滾到舊版本可能需要重新部署整個項目。
- 正確做法是,在開發環境更新依賴(即在本地開發環境執行composer udpate),測試項目依賴更新後的所有功能是否正常。測試無影響後,提交新的 composer.lock 文件到版本控制,確保依賴版本鎖定。在生產環境執行composer install,這將嚴格安裝 composer.lock 中記錄的版本,保證環境一致性,減少因部署的環境不一致而導致的項目運行問題。
參考文章
- Laravel 項目開發規範
- PHP 代碼簡潔之道
- Laravel 編碼技巧
- PHP PSR 標準規範