開發環境
===
- 開發語言
- php 8.4.1
- 資料庫
- MariaDB 10.6.20
- 框架
- laravel 11
- 所有資料庫異動、測試資料或新資料安裝,規定一律透過 Laravel 的 factories, migrations (需包含 up 與 down 功能), seeders, Factory, Schema 進行管控。
- Laravel 套件一律依賴 composer.json,Git 不儲存 vendor 目錄。
- squizlabs/php_codesniffer 套件用於驗證程式碼是否符合 PSR-12 風格。
- API 長時間執行一律使用 Jobs queue 處理並立即返回,不讓前端等待。執行完成後的通知機制,應根據專案需求在開發初期明確定義,可選用 WebSocket 或輪詢等方式。
- 資料傳入驗證,規定統一使用 Illuminate\Foundation\Http\FormRequest 進行。中間層可使用 Middleware 進行判斷,複雜驗證則封裝為 ValidationRule 供 FormRequest 使用。
- 傳入 class 參數,應使用自動注入 (Dependency Injection)。
- 變數一律明定型別。
- Model 關聯應使用 hasMany, belongsTo, hasOne 等方法定義。查詢時必須使用 with() 進行預載入以避免 N+1 問題。為確保查詢效能,在必要時可使用 join,但需經過評估。
- 共用函數統一寫在 helpers.php 檔案中進行管理。
- 共用跨 class 的方法,統一使用 Trait 進行管理。
- 系統應建立快取機制。設定檔等不常變動的資料,應優先從快取 (如 Redis, file) 讀取,避免直接存取資料庫。
- 所有程式方法都必須附上中文註解,清楚說明其用途。
- 每個 API 應使用獨立的 Controller,並以 `__invoke` 方法作為統一進入點。
- Controller 依功能模組區分資料夾,例如:發票 (Invoice)、金流 (Payment)。
- Route 設定檔 (如 payment.php) 應依功能模組區分,並在 `app\Providers\RouteServiceProvider` 的 boot 方法中引入。每個 API 都應有獨立的 route 設定。
- vendor 目錄不提交至 Git。composer.json 中的版本號必須寫死固定版本 (例如 "1.2.3"),禁止使用 `*` 或 `^` (例如 "^1.0"),以確保每次部署時都使用穩定且一致的套件版本。更新套件時,應直接修改版本號。
- 系統
- alpine 3.20
- swoole
- v5.1.2
- octane
- v2.3.6
- bref/bref
- 2.1.19
- linux-firmware-cis
- linux-firmware-cis 3.20版
- 必要模組
- laravel/octane (swoole),參考 https://laravel.com/docs/11.x/octane
- bref/bref
- 程式風格
- PSR-12:規定使用 laravel 套件 squizlabs/php_codesniffer 檢查,通過後才可執行後續測試。
- 變數命名:採小駝峰命名法 (lowerCamelCase),例:`$cashFlowCode = new CashFlowCode();`
- Class 命名:採大駝峰命名法 (UpperCamelCase),例:`class CashFlowCode {}`
- 方法與函數命名:採小駝峰命名法 (lowerCamelCase),且命名必須具體有意義,使開發者能從名稱直接理解其用途。例如,「重新格式化總和記錄」命名為 `reformatSumRecords`。回傳多筆資料時,名稱使用複數 (records);單筆則使用單數 (record)。
- 單獨函數:採 snake_case 風格,即全小寫並以底線 `_` 分隔,例:`reformat_sum_records`。
- 對外API命名規則不在此限,依循對外發布的標準。
- 獨立方法設計:在設計連動方法時,每個方法都必須保持獨立,各自負責單一功能。例如,建立訂單後觸發的「寄信」與「開發票」應為兩個獨立的 Queue Job,各自接收訂單 ID 並撈取所需資料。此設計可確保其中一項任務失敗時,不影響其他任務,並可針對失敗任務單獨重試。
- 錯誤處理
- 通知:透過 `TelegramNotifier` 方法,經由 Google Chat 發送通知。
- 紀錄:使用 `Log::error` 記錄錯誤,內容必須明確標示錯誤的檔案與方法。
- 範例:`CompanyInvoiceService manualPosting 手動入帳觸發交易異常,交易的tmp id:`
- 機敏資訊:禁止在 Log 中記錄任何機敏資訊。若流程需要處理機敏資訊,應將資料存入資料庫,由 Queue Job 讀取執行。若發生錯誤,僅記錄該筆資料的 ID,待修復後,可針對此 ID 進行手動重試。
- 原生方法微調
- Queue 使用 `dispatch` (排隊執行) 或 `dispatch_sync` (立即執行)。若需立即執行但不想等待回應,使用 `dispatch(new Job())->afterResponse()`。
- 如果有排隊要使用,概念是同一個群組依序執行->dispatch(new InventoryCenter('Voucher', []))->onConnection(config('queue.fifo'))->withMessageGroupId('這是群組名');
- 測試
- 測試分為 PHPUnit 的 Feature (整合測試) 與 Unit (單元測試)。
- 所有 API 端點皆須提供 Postman Tests 作為輔助驗證。
- Docker
- 專案需包含 `docker-compose.yml` 檔案,可使用 `docker-compose up -d` 運行及 `docker-compose down` 中止程式。
- 登入API驗證碼
- 前後端統一使用 Cloudflare Turnstile: https://www.cloudflare.com/zh-tw/products/turnstile/
- 開發重要目錄結構說明
```
├─Http
│ ├─Controllers
│ │ ├─Api 負責內部的API
│ │ ├─OuterApi 負責對外的API
│ │ └─WebApi 負責WEB內部的API,底下必須包含自己的處裡目錄
│ │ └─Product 例如:商品相關,再往下細分商品各自的項目,基本上就是2層
│ │ ├─Class 例如:商品分類
│ │ └─Main 例如:商品本身
│ └─Requests 這邊負責validation的定義
│ ├─Api 負責外部的API
│ ├─Constants 各自功能的常數定義
│ ├─Cors 核心常數定義,亦即他的常數是大家公認的例如CashFlowCode
│ └─WebApi 負責內部的API
│ ├─Class 例如:商品分類validation
│ └─Main 例如:商品本身validation
├─Libraries
│ └─Definition 沒有再目錄夾內的為全域共用
│ ├─Common 常數定義檔,不區分內外部,依樣劃分處裡目錄
│ │ └─Product 例如:商品
│ │ └─Main 例如:商品本身
│ │
│ ├─Inputs 內部API請求Request,劃分處裡目錄,如果Request的資料有常數定義,但常數不屬於共用的,也放在這
│ │ └─Product 例如:商品
│ │ ├─Class 例如:商品分類
│ │ └─Main 例如:商品本身
│ │
│ ├─Outputs 內部API回應Response,劃分處裡目錄
│ │ └─Product 例如:商品
│ │ ├─Class 例如:商品分類
│ │ └─Main 例如:商品本身
│ │
│ ├─Requests 外部API請求Request,劃分處裡目錄,如果Request的資料有常數定義,但常數不屬於共用的,也放在這
│ │ └─Product
│ │ ├─Class 例如:商品分類
│ │ └─Main 例如:商品本身
│ │
│ │─Responses 內部API回應Response,劃分處裡目錄
│ │ └─Product 例如:商品
│ │ ├─Class 例如:商品分類
│ │ └─Main 例如:商品本身
│ │
│ └─Services 邏輯處裡層的核心定義,劃分處裡目錄
│ └─Product 例如:商品
│ └─Main 例如:商品本身
│
├─Repositories 資料庫互動處裡,基本上只做資料庫的存取,除非不得已,不然不再此做邏輯處裡
│ └─Api 不區分目錄
│
└─Services 邏輯處裡層
│
├─Core 邏輯核心處哩,這部份主要是處里內外層都會用到的處理邏輯,並將請求跟輸出的格式都定義清楚,嚴格依照定義的格式內容,每個定義的欄位都需要寫好預設值
│ └─Product 例如:商品
│ └─Main 例如:商品本身
│
├─Validation 負責定義檔的驗證
│ ├─Api 負責外部的API
│ └─WebApi 負責內部的API,底下必須包含自己的處裡目錄
│ └─Product 例如:商品
│ └─Main 例如:商品本身
│
├─WebApi 接收內部請求的邏輯處裡,基本上都是將接收的資料,轉成給邏輯核心處理,核心處理完後,輸出的結果若有要調整,也是在自己的內部處理調整
│ └─Product 例如:商品
│ └─Main 例如:商品本身
│
└─Api 接收外部請求的邏輯處裡,基本上都是將接收的資料,轉成給邏輯核心處理,核心處理完後,輸出的結果若有要調整,也是在自己的內部處理調整
```
- Drone (CI/CD) //現階段人手不足放棄
- **觸發**:利用 Drone 連結 GitHub,當 git push 事件發生後,自動觸發 CI/CD 流程。
- **部署目標**:依 git flow 規則,`staging` 分支對應測試區,`master` 分支對應正式區。
- **CI 流程**:
1. 驗證 PHPCS (PSR-12 風格)。
2. `install-packages` (安裝 Laravel 套件)。
3. 執行 PHPUnit Feature (整合測試)。
4. 執行 PHPUnit Unit (單元測試)。
5. 所有測試通過後,方可進行 CD 流程。
- **CD 流程**:
1. 封裝 Image 並上傳至 Docker Hub。
2. 部署至對應的正式或測試環境。
3. 上傳最新的 Swagger yml 至 Swagger Server。
- **部署策略**:部署流程中的資料庫遷移 (migrations) 規定採用手動執行模式,以確保生產環境的穩定與可控性。自動化遷移僅限於測試或開發環境。
- **版本管理**:Docker Hub 用於存放各版本 Image,當線上發生問題時,可手動選擇特定版本進行退版,以維持服務穩定。
- API文檔說明
- 文檔規定使用 Swagger (OpenAPI) 撰寫 yml 檔案,並提供 Swagger Server 進行線上即時測試與查閱回應內容。
- WEB API基本傳入格式
- `rqTS`:傳入時間,格式為 `(GMT+8)2015-12-02 04:34:55`。
- `cmd`:指令名稱。
- `locale`:資料交換語系。
- WEB API 當有需要時
- `search`:搜尋條件。
- `pagination`:分頁設定,格式為 `{"perPage": 10, "currentPage": 1}`。
- WEB API回傳格式
- **HTTP Status Code**:嚴格遵守 RESTful API 標準。
- `200`:請求成功。
- `201`:資源建立成功。
- `202`:請求已接收但未完成 (用於異步任務)。
- `204`:請求成功但無內容回傳。
- `3xx`:重定向。
- `4xx`:客戶端錯誤。
- `5xx`:伺服器錯誤。
- **Body 結構**:
- `code`:API 內部狀態碼 ([200]正常, [100]資料不正確, [210]處理中, [300]處理失敗, [400]系統錯誤,[401]無效的登入)。若有新定義需經討論。
- `msg`:回應訊息。
- `rspTS`:回應時間,格式同 `rqTS`。
- `cmd`:同傳入指令。
- `locale`:同傳入語系。
- `errors`:錯誤細節,當 `code=300` 時提供。
- `data`:回傳資料。無資料時回傳 `null`。除了登入 API,所有 `data` 內容均需使用 token 進行加密。
- `exp`:回傳登出剩餘時間。
- `errors` 格式 (當 `code=300`)
- `code` (string): 錯誤代碼。
- `msg` (string): 錯誤訊息。
- `pathData` (array): 指出發生錯誤的傳入值路徑。
- `path` (string): 欄位路徑,例如 `a3:aa1`。
- `msg` (array): 錯誤的詳細資訊。
- **範例**:
- 傳入值:`{"a1":1,"a2":2,"a3":[{"aa1":"AA"}]}`,其中 `aa1` 名稱重複。
- 回應:`{"code":"A0001","msg":"名稱重複","pathData":[{"path":"a3:aa1","msg":["AA"]}]}`
- 加密規則
- **PHP (Server-side)**:
- 使用 `openssl_encrypt` 和 `openssl_decrypt`。
- `iv` 固定為登入 API 回傳 token 的前 16 碼。
- 範例: `openssl_encrypt($text, $method, $secret, 0, $iv);`
- **JavaScript (Client-side)**:
- 使用 CryptoJS。
- `iv` 同樣固定為登入 API 回傳 token 的前 16 碼。
- validation rule 規則
- 在撰寫 FormRequest validation 時,若遇到無法用單純 rule 處理的複雜情境,應使用 `withValidator` 或 `after` 方法。
- **範例一:根據 type 決定 content 的型別**
```php
public function withValidator($validator)
{
$validator->sometimes('content', 'array', fn($input) => $input->type == 1);
$validator->sometimes('content', 'string', fn($input) => $input->type == 2);
}
```
- **範例二:根據陣列中某個值,決定另一個值是否必填**
```php
public function withValidator($validator)
{
$data = $this->input('data');
foreach ($data as $index => $item) {
$validator->sometimes("data.$index.cell_phone", 'required', fn($input) => $item && $item['phone_type'] === 1);
$validator->sometimes("data.$index.phone", 'required', fn($input) => $item && $item['phone_type'] === 2);
}
}
```
- **範例三:根據不同條件,提供不同錯誤訊息**
```php
public function withValidator($validator)
{
$validator->after(function ($validator) {
$data = $this->get('data') ?? [];
foreach ($data as $index => $item) {
if ($item && $item['type'] === 1 && !in_array($item['mode'], [1, 2])) {
$validator->errors()->add("data.mode", trans('error.mode_invalid_for_type1'));
}
if ($item && $item['type'] === 2 && !in_array($item['mode'], [1, 2, 3])) {
$validator->errors()->add("data.mode", trans('error.mode_invalid_for_type2'));
}
}
});
}
```
- 常數共用
- `MyPayAllPayModeForNumber`: 記錄所有支付工具的<span style="color:red;">數字常數</span>。
- `MyPayAllPayModeForString`: 記錄所有支付工具的<span style="color:red;">字串常數</span>。
- `CashSupportPayMode`: 定義現金交易類型支援的支付工具。
- `CashSupportPayModeGroup`: 現金類支付工具的群組化定義。
- **規則**:所有支付工具相關的常數,均須以此四個檔案為唯一來源,不得在其他地方另行定義。
- 程式命名規定
- **關於時間**
- 起始時間: `startDate`
- 結束時間: `endDate`
- **狀態 (State vs. Status)**
- `State`: 代表程式內部的運作狀態 (e.g., `isPlaying`)。
- `Status`: 代表向使用者顯示的結果 (e.g., 「正在播放」)。
- **規則**:若同時用於內部程序與外部顯示,則統一使用 `status`。
- **Class 命名**
- **規則**:必須使用名詞,並依據功能使用以下強制定義的後綴。
- `list`: 分類
- `creator`: 新增
- `update`: 更新
- `delete`: 刪除
- `item`: 單筆資訊
- `import`: 匯入
- `view`: 純檢視 (後台,無權限)
- `frontendView`: 純檢視 (前台)
- `download`: 下載
- `inquirer`: 查詢
- `export`: 匯出
- `store`: 資料儲存 (新增/修改)
- `changeStatus`: 改變狀態
- `front`: 無需登入的前台功能
- 資料庫命名
- **資料表**
- **規則**:名稱一律使用複數,以 `s` 結尾。
- **關於時間**
- 起始時間: `start_date`
- 結束時間: `end_date`
- **狀態 (State vs. Status)**
- `State`: 內部運作狀態。
- `Status`: 外部顯示結果。
- **規則**:若同時使用,統一用 `status`。
- **其它**
- 操作者: `operator` (關聯 `users->uid`)