Stories

Detail Return Return

Laravel 項目開發規範 - Stories Detail

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 變量
  1. 使用有意義且可讀的變量名,變量名遵循駝峯命名規範。

    // 壞代碼示例
    $date = date('y-m-d');
    
    // 好代碼示例
    $currentDate = date('y-m-d');
    
    // 從變量名可知,$date 是一個日期,但不知是當天日期還是某個特定時間的日期,相比於 $date,$currentDate更能體現其具體使用場景。
  2. 不要添加不需要的上下文。如果你的 類或對象 已經有明確的含義,不要在變量中再次重複它。

    // 壞代碼
    class Car
    {
        public $carMake;
    
        public $carModel;
    
        public $carColor;
    
        //...
    }
    
    // 好代碼
    class Car
    {
        public $make;
    
        public $model;
    
        public $color;
    
        //...
    }
4.1.2 分支判斷
  1. 避免嵌套太深(最多不超過三層)並儘早返回。

    # 壞代碼示例
    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 函數
  1. 函數名應該語義化。

    // 壞代碼示例
    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();
  2. 函數參數最多不應超過3個。

    • 一旦一個方法擁有三個以上的參數將會導致組合爆炸,在進行測試時需要使用每個單獨的參數測試大量不同的情況。
    • 零參數是理想的情況。 一兩個參數是可以的,三個應該避免。除此之外的任何東西都應該合併
    • 通常,如果您有兩個以上參數這説明你的函數做的事情太多了。如果不是,大多數情況下一個更高級別的對象就足以作為一個參數。
  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);
    }
  4. 避免副作用。避免副作用意味着,儘量不要再函數內使用全局變量或者使用引用傳遞的參數。

    // 壞代碼示例
    // 通過以下函數引用的全局變量。
    // 如果我們有另外一個使用這個名字函數,現在它將是一個數組,它可能會破壞它。
    $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'];
  5. 封裝條件,且函數命名時避免使用否定條件。

    // 壞代碼示例
    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 配置信息與環境變量

  1. 因 .env 不會被納入版本控制器中,所以本地 .env 文件裏添加變量時必須同步到 .env.example 文件中,以免影響其他項目參與者的工作。
  2. 所有程序配置信息必須通過 config() 來讀取,所有的 .env 配置信息必須通過 env() 來讀取,不在配置文件以外的範圍使用 env()。原因在於:

    • 定義分明,config() 是配置信息,env() 只是用來區分不同環境。
    • 統一放置於 config 中還可以利用框架的配置信息緩存功能(php artisan config:cahce)來提高運行效率,當配置信息被緩存後,.env 文件將不會被加載,所以env() 函數將讀取不到 .env 文件中的內容。
    • 代碼健壯性, config() 在 env() 之上多出來一個抽象層,會使代碼更加健壯,更加靈活。

4.3 路由

  1. 不在路由定義文件中書寫「閉包路由」或者其他業務邏輯代碼,因為路由緩存 並不會作用在基於閉包的路由。
  2. 路由定義文件中要保持乾淨整潔,絕不放置除路由配置以外的其他程序邏輯。
  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');
    });
  4. 優先使用 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 控制器

  1. 優先使用Restful 資源控制器。
  2. 控制器命名規範,必須使用資源的複數形式,如:

    • 類名:PhotosController
    • 文件名:PhotosController.php
  3. 在控制器內不能聲明私有方法,控制器內所有的方法都是外部可訪問的表示路由動作的公有方法,並且控制器裏的所有方法,都應該被使用到,否則應該刪除,也絕對不在控制器裏批量註釋掉代碼,無用的邏輯代碼就必須清除掉。。
  4. 控制器內不應包含業務邏輯,業務邏輯的實現應該在業務邏輯層去實現。
  5. 不應該為「方法」書寫很明顯的註釋,這要求方法取名要足夠合理,不需要過多註釋。應該為一些複雜的邏輯代碼塊書寫註釋,主要介紹產品邏輯 - 為什麼要這麼做。
  6. 表單數據校驗應該放到控制器層(如果是非複雜表單的字段檢驗,可直接在方法內進行校驗,否則需單獨寫一個表單驗證類),驗證通過後再將請求轉發給業務邏輯層。

    /**
     * 存儲一篇新的博客文章。
    *
    * @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 表單驗證

  1. 使用表單請求 - FormRequest 類 來處理控制器裏的表單驗證。
  2. 驗證類命名規則,FormRequest表驗證類命名必須按照控制器方法來命名,且必須添加模型的前綴,類名稱的 Request 後綴也是必須的,這方便了編輯器開始打開文件。如:

    • UserCreateRequest
    • UserUpdateRequest

4.6 業務邏輯層(Service 層或 Logic 層)

  1. Controller 層只接收和轉發請求,Model 層只負責模型定義以及數據關聯邏輯。由業務邏輯層負責處理業務邏輯。
  2. Service 中的方法命名參考 Laravel Model 中的方法命名。
  3. 所有的 Service 類都必須存放於 app/Services 目錄中,且應避免直接將 Service 類放置於 app/Services 目錄下,應該考慮通過業務邏輯,將其歸類於子目錄中。

    Auth —— 存放登錄、授權相關的 Service
    Payment —— 存放支付相關的 Service
    Book —— 存放課程相關的 Service
  4. Service 類必須是無狀態的,無狀態意味着無論是控制器方法中、命令行、單元測試中,都可調用。

4.7 模型

  1. 所有的數據模型文件,都必須存放在:app/Models/ 文件夾中。且所有的 Eloquent 數據模型都必須繼承統一的基類 App\Models\Model,此基類存放位置為 /app/Models/Model.php。
  2. 命名規範

    • 數據模型類名必須為「單數」,如: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
  3. 目錄分層:為模型文件按業務邏輯做分層。
  4. 儘量避免使用 Laravel 的 模型事件。使用模型事件的問題在於,其職能很難界定,所有的業務邏輯都能寫到模型事件中。

4.8 視圖

  1. 避免在 resources/views 目錄下直接放置視圖文件。頁面佈局文件必須放在 resources/views/layouts 目錄下,頁面組件必須放在 resources/views/common 目錄下,簡單頁面(如404頁面)放在 resources/views/pages 目錄下。對應 Restful 路由的資源路徑名稱,例如以URI photos/create 為例,對應的 create.blade.php 文件,應存放在 resources/views/photos 目錄下。
  2. 局部視圖文件必須使用 <kdb>_</kdb> 前綴來命名,如:photos/_upload_form.blade.php
  3. 為了和 Restful 路由器和資源控制器保持一致,視圖命名也必須使用資源視圖的命名方式。例如, 一個完整的 photos 資源對應的視圖文件為以下:

    ├── photos
    │   ├── _form.blade.php
    │   ├── create.blade.php
    │   ├── edit.blade.php
    │   ├── index.blade.php
    │   └── show.blade.php

5. Git 工作流規範

6. 服務器部署規範

6.1 Composer 依賴安裝規範

  1. 生產環境或預發佈環境進行項目部署時,必須使用 --no-dev 來安裝項目運行必需依賴。如安裝 predis 擴展:

    composer require predis/predis --no-dev
  2. 本地環境或測試環境安裝開發專用擴展時,必須使用 --dev 來安裝開發依賴。如在本地安裝 phpunit 擴展:

    composer require phpunit/phpunit --dev

6.2 Composer 依賴版本更新

  1. 絕對不能在生產服務器執行,composer udpate,原因如下:

    • composer udpate 會根據 composer.json 文件的版本約束拉取最新兼容版本,可能導致依賴包升級到未測試的版本,引入不兼容的變更或bug。同時,這將導致開發環境與生產環境的依賴版本可能因此不一致,當系統出現問題時,排查問題變得困難。
    • 由於依賴包的升級可能導致項目運行報錯。
    • 執行composer udpate後,由於依賴已全局更新,回滾到舊版本可能需要重新部署整個項目。
  2. 正確做法是,在開發環境更新依賴(即在本地開發環境執行composer udpate),測試項目依賴更新後的所有功能是否正常。測試無影響後,提交新的 composer.lock 文件到版本控制,確保依賴版本鎖定。在生產環境執行composer install,這將嚴格安裝 composer.lock 中記錄的版本,保證環境一致性,減少因部署的環境不一致而導致的項目運行問題。

參考文章

  • Laravel 項目開發規範
  • PHP 代碼簡潔之道
  • Laravel 編碼技巧
  • PHP PSR 標準規範
user avatar laoduan Avatar tpwonline Avatar wujingquan Avatar skyselang Avatar yujiaao Avatar tim_xiao Avatar zero_dev Avatar invalidnull Avatar xingzoudedahuoji Avatar zjkal Avatar yanwushu Avatar seth9shi Avatar
Favorites 32 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.