🧠 PSR-1:基本編碼標準
理解與應用
請說明 PSR-1 的兩大核心原則是什麼?為什麼它們重要?
核心原則一:程式碼必須自動載入相容(Autoloading Compatibility)。
內容: 類別命名必須遵循特定規範(如
StudlyCaps),命名空間必須與檔案路徑對應。重要性: 這是現代 PHP 專案的基石。它確保了 Composer 和其他自動載入器能可靠地找到並載入類別檔案,從根本上解決了 PHP 過去
require/include依賴混亂的問題,提升了專案的模組化和可維護性。
核心原則二:程式碼風格統一。
內容: 包含但不限於:使用
<?php或<?=標籤;一個檔案中只應定義類別/函式/常數,或只應執行副作用邏輯;類別常數使用全大寫與底線;方法名稱使用camelCase。重要性: 它提供了一個所有 PHP 專案都可以接受的最低標準,確保了不同開發者和專案之間的程式碼視覺和結構一致性,極大提升了程式碼的可讀性和協作效率。
你如何確保你的程式碼遵守 PSR-1 的「自動載入相容性」?請舉例說明命名空間與檔案結構的對應方式。
我會採用 PSR-4 標準來確保自動載入相容性,因為 PSR-4 是 PSR-1 的具體實踐。
步驟:
設定 Composer: 在
composer.json中設定autoload區塊,定義命名空間前綴與其對應的基礎目錄。遵循規範: 確保所有類別(
class、trait、interface)都使用StudlyCaps命名法。對應範例:
命名空間前綴:
Acme\Blog\對應目錄:
src/類別完整名稱:
Acme\Blog\Post\ArticleModel檔案路徑:
src/Post/ArticleModel.php
原理: Composer 會將命名空間前綴
Acme\Blog\替換成基礎目錄src/,剩下的子命名空間Post對應到子目錄,最終的類別名稱ArticleModel對應到檔案名稱ArticleModel.php。
在多人協作的專案中,如何落實 PSR-1 的「程式碼文件必須使用 UTF-8 無 BOM」原則?你有遇過相關問題嗎?
落實方式:
版本控制(Git): 在
.editorconfig檔案中加入charset = utf-8和trim_trailing_whitespace = true等設定,引導編輯器儲存正確的格式。程式碼審查(Code Review): 在 Code Review 階段檢查檔案編碼是否正確,特別是新加入的檔案。
CI/CD 工具: 在 CI/CD 流程中加入靜態分析工具(如 PHP_CodeSniffer),檢查文件是否包含 BOM 標記。
遇過的問題:
問題: 早期開發者使用舊版或配置不當的 Windows 編輯器,導致部分檔案被儲存成帶有 UTF-8 BOM 的格式。
後果: 儘管 BOM 不可見,但它會被 PHP 解析器視為輸出內容,尤其在檔案開頭,會導致以下問題:
Header Sent 錯誤: 在類別載入時,BOM 會提前發送內容給瀏覽器,導致後續的
header()、setcookie()或 Session 啟動失敗,拋出Cannot modify header information - headers already sent by...錯誤。JSON/XML 解析錯誤: 輸出 JSON 或 XML 時,開頭的 BOM 會讓客戶端解析失敗。
解決: 使用
git ls-files | xargs grep -l $'\xEF\xBB\xBF'等命令找出含有 BOM 的檔案,並使用iconv或 VS Code 等工具將其轉換成無 BOM 格式。
🧩 PSR-2:程式碼風格指南(已被 PSR-12 取代,但仍常見)
實務與細節
請說明 PSR-2 對縮排、括號、空格的具體要求。你在 IDE 或 CI/CD 中如何強制執行這些規則?
要求說明:
| 元素 | PSR-2 具體要求 |
| :--- | :--- |
| 縮排 (Indentation) | 必須使用 4 個空格 (Spaces),而非 Tab。 |
| 括號 (Braces) | 類別/方法 的左括號必須在下一行開始;控制結構 (如 if/else/for/while) 的左括號必須在同一行開始。右括號必須在獨立的一行。 |
| 空格 (Spacing) | 關鍵字後必須有一個空格 (e.g., if ();運算子 (如 =, +, ==) 兩側必須有一個空格;方法參數的逗號後必須有空格。 |
強制執行:
IDE: 我會使用 PHPStorm 或 VS Code,並安裝 PHP CS Fixer 或 PHP CodeSniffer 擴充功能,設定其使用
PSR2或PSR12規則集,並啟用儲存時自動修正。CI/CD: 在 CI Pipeline 中,我會使用 PHP_CodeSniffer (phpcs) 搭配
--standard=PSR2或PSR12旗標,在 Git Push 或 Pull Request 時執行靜態分析。如果程式碼不符合規範,Pipeline 將會失敗,阻止合併。
PSR-2 規定每行最多 80 字,最多 120 字。你在實務上如何處理過長的函式呼叫或陣列?
基本原則: 優先將行寬控制在 80 字元內,120 字元作為硬性上限。
處理過長函式呼叫 (Method Call):
將參數斷行,每個參數獨立一行,並使用四個空格縮排。
PHP// 不佳 $user = $this->userService->createUser( $request->get('name'), $request->get('email'), $request->get('password'), $request->get('role') ); // 遵循 PSR-2/12 $user = $this->userService->createUser( $request->get('name'), $request->get('email'), $request->get('password'), $request->get('role') );處理過長陣列 (Array):
將陣列中的每個元素獨立一行,逗號保留在元素之後,並對齊。
PHP$config = [ 'database' => 'mysql', 'host' => 'localhost', 'username' => 'user', 'password' => 'secret', ];
請舉一段不符合 PSR-2 的程式碼,並改寫成符合規範的版本。
不符合 PSR-2 的程式碼 (常見的 K&R 或自訂風格):
PHPclass userModel { public function getData($id, $format='json') { if ($id <= 0){ throw new Exception("Invalid ID"); } $this->data = $this->db->getRecord($id); } }符合 PSR-2 的程式碼 (遵循類名
StudlyCaps、方法名camelCase、括號位置、空格、縮排):PHPclass UserModel { public function getData($id, $format = 'json') { if ($id <= 0) { throw new \Exception('Invalid ID'); } $this->data = $this->db->getRecord($id); } }
🧱 PSR-3:日誌介面
架構與擴充性
PSR-3 定義了哪些 log level?你在什麼情境下會使用
alert而不是critical?Log Level (嚴重性由高到低):
emergencyalertcriticalerrorwarningnoticeinfodebug
alertvs.critical的情境區分:critical(系統關鍵錯誤): 指的是應用程式本身的錯誤,通常是無法預期且需要立即關注的故障,但系統的核心功能可能仍在運行。情境範例: 資料庫連線中斷、一個主要的微服務超時、程式碼拋出致命的
Exception導致單次請求失敗。
alert(緊急行動警報): 指的是必須立即採取行動,因為它影響了整個系統的可用性或重要性。它比critical更具外部緊急性。情境範例: 整個網站/服務下線、支付閘道完全無法響應、監控系統發現主硬碟已滿 (
disk full)、備份失敗。記憶方法:
alert是給 Ops 團隊的,critical是給 Dev 團隊的。alert應該觸發即時的 Pager/SMS 警報。
你如何在 Laravel 或 Symfony 中整合 PSR-3 相容的 logger?請說明具體做法。
Laravel:
做法: Laravel 預設的 Log 服務是透過 Monolog 實現的,而 Monolog 完全符合 PSR-3 規範。因此,Laravel 的
LogFacade 或透過 Dependency Injection 注入的\Psr\Log\LoggerInterface實例,可以直接使用 PSR-3 定義的八個方法 (如info(),error(),debug())。範例:
PHP// 透過 Facade \Illuminate\Support\Facades\Log::error('使用者登入失敗', [ 'user_id' => $userId, 'ip' => $request->ip() ]); // 透過 Dependency Injection (在建構函式中注入) use Psr\Log\LoggerInterface; class SomeService { protected $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function process() { $this->logger->warning('API 響應時間過長', ['time' => 3000]); } }
Symfony:
做法: Symfony 預設使用 MonologBundle,它會自動將 Monolog 註冊為
logger服務,並實作\Psr\Log\LoggerInterface。可以直接透過 Autowiring (自動裝配) 或在服務定義中取得它。範例:
PHP// 在 Controller 或 Service 中使用 Autowiring use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; class PaymentController extends AbstractController { public function checkout(LoggerInterface $logger) { // ... 處理邏輯 $logger->notice('使用者成功結帳', ['order_id' => $orderId]); } }
如果你要替換 Monolog 為自製 logger,如何確保相容 PSR-3?請寫出介面實作的範例。
確保相容性: 只要自製的 Logger 實作
\Psr\Log\LoggerInterface介面,並遵循介面中定義的八個日誌方法 (emergency到debug) 以及核心的log(string $level, string $message, array $context = [])方法,就能保證相容 PSR-3。介面實作範例:
PHP<?php namespace App\Logging; use Psr\Log\LoggerInterface; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; class CustomFileLogger implements LoggerInterface { protected $logFile; public function __construct(string $logFile) { $this->logFile = $logFile; } // 核心方法:所有級別方法都會調用它 public function log($level, $message, array $context = []) { // 1. 驗證 $level 是否為有效的 LogLevel (PSR-3 要求) if (!in_array($level, $this->getValidLevels())) { throw new InvalidArgumentException("無效的日誌級別: {$level}"); } // 2. 處理 Context (將佔位符替換成 Context 內容) $message = $this->interpolate($message, $context); // 3. 寫入日誌檔案 $logEntry = sprintf( "[%s] [%s] %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message ); file_put_contents($this->logFile, $logEntry, FILE_APPEND); } // 實作所有介面方法 (將呼叫導向 log() 核心方法) public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } // 輔助方法:將 {placeholder} 替換為實際值 (PSR-3 推薦的行為) protected function interpolate($message, array $context): string { // 簡單實作,正式環境會更嚴謹 $replace = []; foreach ($context as $key => $val) { if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { $replace['{' . $key . '}'] = $val; } } return strtr($message, $replace); } protected function getValidLevels(): array { return [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG]; } }
🏗️ PSR-4:自動載入標準
架構與維護性
請說明 PSR-4 的命名空間與目錄結構對應原則。與 PSR-0 有何差異?
PSR-4 原則 (推薦標準):
定義一個命名空間前綴 (Namespace Prefix)。
這個前綴必須對應到一個基礎目錄 (Base Directory)。
命名空間前綴之後的子命名空間,必須精確對應到基礎目錄之後的子目錄。
類別名稱 (Class Name) 必須對應到以
.php結尾的檔案名稱。
PSR-4 與 PSR-0 的差異 (關鍵):
| 特性 | PSR-4 (Current) | PSR-0 (Deprecated) |
| :--- | :--- | :--- |
| 命名空間前綴 | 必須對應到基礎目錄。 | 可以對應到任何目錄。 |
| Vendor 前綴 | 不要求必須在檔案結構中體現。 | 必須包含 Vendor Name,且 Vendor Name 必須對應到實體目錄。 |
| 下底線 (_) | 不處理。通常不建議用於命名空間或類別名。 | 會被視為目錄分隔符。 |
| 核心優勢 | 簡化了檔案結構,移除不必要的 Vendor 或 Root 命名空間目錄層級,提高了自動載入的效率。 | 檔案結構可能過深,且對 Vendor/子目錄的處理較為僵化。 |
你如何在 composer.json 中設定 PSR-4 的 autoload?請舉例說明。
我會將專案的主要應用程式碼放在
src目錄,而將測試程式碼放在tests目錄。composer.json範例:JSON{ "name": "my-vendor/my-project", "autoload": { "psr-4": { "App\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "require": {} }執行: 執行
composer dump-autoload後,Composer 會產生vendor/autoload.php和vendor/composer/autoload_psr4.php等檔案,建立命名空間到檔案路徑的對應表。
如果你有一個命名空間
App\Services\Payment,請說明它應該對應到哪個檔案路徑?如何驗證 autoload 是否正確?情境: 假設我的
composer.json設定是"App\\": "src/"。對應檔案路徑:
命名空間前綴
App\對應到基礎目錄src/。剩餘子命名空間
Services\Payment對應到子目錄Services/Payment/。如果類別名稱是 App\Services\Payment\StripeGateway,則其檔案路徑應該是:
$$\text{src/Services/Payment/StripeGateway.php}$$
驗證 Autoload:
手動驗證 (Class Exist): 在專案的啟動檔案中 (
index.php或public/index.php),載入 Composer 的 autoload 檔案,然後檢查類別是否存在。PHPrequire 'vendor/autoload.php'; // 檢查類別是否存在,如果不存在,Autoloading 就失敗了 if (class_exists(\App\Services\Payment\StripeGateway::class)) { echo "Autoload 成功!"; } else { echo "Autoload 失敗!"; }CLI 驗證 (Composer Validate/Diagnose): 使用 Composer 內建工具檢查設定。
Bashcomposer validate # 檢查 composer.json 語法及設定是否正確使用測試 (Unit Test): 這是最可靠的方式。如果一個 Unit Test 需要載入這個類別才能執行,而測試通過,就表示 Autoloading 正確。
🔍 加分題:整合與實務挑戰
你如何在 CI/CD 流程中自動檢查 PSR-1~4 的遵守情況?用哪些工具?
我會使用 PHP_CodeSniffer (PHPCS) 和 PHP-CS-Fixer 這兩個業界標準工具。
流程:
設定階段:
安裝工具:
composer require --dev squizlabs/php_codesniffer friendsofphp/php-cs-fixer設定 PHPCS: 建立一個
phpcs.xml或使用 CLI 配置,指定使用PSR12規則集(PSR-12包含並取代了PSR-1和PSR-2)。同時,PSR-4的規則檢查也包含在內。
CI/CD Pipeline (例如 GitLab CI 或 GitHub Actions):
步驟一:風格檢查 (Linter / Sniffer)
目的: 找出不符合風格規範的程式碼,並強制失敗。
指令:
vendor/bin/phpcs --standard=PSR12 src tests結果: 不允許不符合 PSR-1/2/4 的程式碼被合併。
步驟二:自動修正 (Fixer)
目的: 針對可以自動修正的風格問題,在開發環境或專門的 CI Job 中進行修正,並將修正結果寫回專案。
指令:
vendor/bin/php-cs-fixer fix src tests --rules=@PSR12
Hooks (加分): 搭配 GrumPHP 或 Pre-Commit Hook,在開發者
git commit之前執行 Linter,提早發現並修正錯誤。
你曾經在大型專案中推動 PSR 標準嗎?遇到哪些阻力?如何解決?
經歷: 是的,我在一個從舊版 PHP 升級到 PHP 7+ 的大型專案中推動過 PSR-1/2/4 (後續升級為 PSR-12) 標準化。
遇到的阻力:
歷史遺留問題 (最大的阻力): 專案中數十萬行的程式碼使用了 K&R 風格、Tabs 縮排,並且許多類別不符合 PSR-4 命名規範。一次性修改成本高昂且風險極大。
資深開發者習慣: 某些資深工程師對自己的風格有強烈的偏好,認為 PSR 規範「囉嗦」、「不實用」,抗拒改變手寫習慣。
新舊程式碼衝突: 在推動過程中,新舊程式碼風格不一致,導致視覺混亂,反而影響了可讀性。
解決方案:
分階段實施:
遺留程式碼 (Legacy Code): 暫時排除風格檢查,或只檢查最核心的 PSR-1/4 (命名規範)。
新程式碼 (New Code): 在 CI/CD 中設定硬性規則:所有新的或被修改的檔案必須通過 PSR-12 檢查。
自動化工具導入:
強制使用 PHP-CS-Fixer: 不依賴人工記憶,要求所有人在 Commit 前執行風格修正指令。最好將其整合到 Git Hook 或 IDE 的「儲存時自動格式化」功能。
單次大規模重構: 對於少部分核心且穩定的遺留程式碼,在專門的排程時間進行一次性大規模的
php-cs-fixer跑遍,然後進行嚴格的迴歸測試。
教育與溝通: 強調 PSR 的價值在於**「降低認知負擔」和「提升團隊效率」**,而不是美學或個人偏好。透過 Code Review 示範不一致風格帶來的閱讀成本,讓團隊成員體會到統一標準的好處。
PSR-1~4 如何幫助你提升程式碼的可維護性與可讀性?請用實際經驗說明。
PSR-4 (自動載入) 提升可維護性:
經驗: 過去的專案使用
require_once,當類別重命名或移動時,必須手動查找並修改所有依賴檔案。助益: PSR-4 + Composer 實現了零配置的類別載入。當我看到
new App\Services\AuthService()時,我能立刻且準確地知道它在檔案系統中的位置 (src/Services/AuthService.php),反之亦然。這極大地簡化了專案導航和重構的過程,提升了專案的架構清晰度。
PSR-1/2/12 (風格標準) 提升可讀性:
經驗: 曾接手一個專案,風格混亂,有時用
tab有時用space,括號位置不一,變數命名各異。每次讀新程式碼都需要先「適應」作者的風格。助益: 統一的風格就像一個視覺合約。當我閱讀任何遵循 PSR 的程式碼時,我的大腦不需要花費資源去解析風格差異。我知道
if之後會有一個空格,我知道類別名稱會是StudlyCaps。這使我能將全部的認知資源集中在程式碼的業務邏輯本身,大幅提升了閱讀速度、理解深度和偵錯效率。
PSR-3 (日誌介面) 提升架構彈性:
經驗: 專案初期使用簡單的
file_put_contents寫日誌。後來需要升級到 ELK Stack 集中式日誌系統。助益: 由於程式碼中所有的日誌記錄都依賴
LoggerInterface(PSR-3),我只需要在 IoC 容器中將Monolog替換成一個自定義的ElasticsearchLogger(實作LoggerInterface),而不需要修改任何一處業務邏輯中的Log::error()呼叫。這保證了日誌系統的可替換性,是微服務或大型專案中不可或缺的解耦手段。
整體來說,這套問題完美地檢驗了從基礎規範到實際操作,再到架構思維和團隊協作的各個層面,展示了對 PHP 標準化運動的全面理解。
沒有留言:
張貼留言