2025年10月24日 星期五

🚀 資深 PHP 工程師的 PSR 深度解析:從規範到實戰架構

🧠 PSR-1:基本編碼標準

理解與應用

  1. 請說明 PSR-1 的兩大核心原則是什麼?為什麼它們重要?

    • 核心原則一:程式碼必須自動載入相容(Autoloading Compatibility)。

      • 內容: 類別命名必須遵循特定規範(如 StudlyCaps),命名空間必須與檔案路徑對應。

      • 重要性: 這是現代 PHP 專案的基石。它確保了 Composer 和其他自動載入器能可靠地找到並載入類別檔案,從根本上解決了 PHP 過去 require/include 依賴混亂的問題,提升了專案的模組化和可維護性。

    • 核心原則二:程式碼風格統一。

      • 內容: 包含但不限於:使用 <?php<?= 標籤;一個檔案中只應定義類別/函式/常數,或只應執行副作用邏輯;類別常數使用全大寫與底線;方法名稱使用 camelCase

      • 重要性: 它提供了一個所有 PHP 專案都可以接受的最低標準,確保了不同開發者和專案之間的程式碼視覺和結構一致性,極大提升了程式碼的可讀性協作效率

  2. 你如何確保你的程式碼遵守 PSR-1 的「自動載入相容性」?請舉例說明命名空間與檔案結構的對應方式。

    • 我會採用 PSR-4 標準來確保自動載入相容性,因為 PSR-4 是 PSR-1 的具體實踐。

    • 步驟:

      1. 設定 Composer:composer.json 中設定 autoload 區塊,定義命名空間前綴與其對應的基礎目錄。

      2. 遵循規範: 確保所有類別(classtraitinterface)都使用 StudlyCaps 命名法。

      3. 對應範例:

        • 命名空間前綴: Acme\Blog\

        • 對應目錄: src/

        • 類別完整名稱: Acme\Blog\Post\ArticleModel

        • 檔案路徑: src/Post/ArticleModel.php

    • 原理: Composer 會將命名空間前綴 Acme\Blog\ 替換成基礎目錄 src/,剩下的子命名空間 Post 對應到子目錄,最終的類別名稱 ArticleModel 對應到檔案名稱 ArticleModel.php

  3. 在多人協作的專案中,如何落實 PSR-1 的「程式碼文件必須使用 UTF-8 無 BOM」原則?你有遇過相關問題嗎?

    • 落實方式:

      1. 版本控制(Git):.editorconfig 檔案中加入 charset = utf-8trim_trailing_whitespace = true 等設定,引導編輯器儲存正確的格式。

      2. 程式碼審查(Code Review): 在 Code Review 階段檢查檔案編碼是否正確,特別是新加入的檔案。

      3. 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 取代,但仍常見)

實務與細節

  1. 請說明 PSR-2 對縮排、括號、空格的具體要求。你在 IDE 或 CI/CD 中如何強制執行這些規則?

    • 要求說明:

      | 元素 | PSR-2 具體要求 |

      | :--- | :--- |

      | 縮排 (Indentation) | 必須使用 4 個空格 (Spaces),而非 Tab。 |

      | 括號 (Braces) | 類別/方法 的左括號必須在下一行開始;控制結構 (如 if/else/for/while) 的左括號必須在同一行開始。右括號必須在獨立的一行。 |

      | 空格 (Spacing) | 關鍵字後必須有一個空格 (e.g., if ();運算子 (如 =, +, ==) 兩側必須有一個空格;方法參數的逗號後必須有空格。 |

    • 強制執行:

      • IDE: 我會使用 PHPStormVS Code,並安裝 PHP CS FixerPHP CodeSniffer 擴充功能,設定其使用 PSR2PSR12 規則集,並啟用儲存時自動修正

      • CI/CD: 在 CI Pipeline 中,我會使用 PHP_CodeSniffer (phpcs) 搭配 --standard=PSR2PSR12 旗標,在 Git Push 或 Pull Request 時執行靜態分析。如果程式碼不符合規範,Pipeline 將會失敗,阻止合併。

  2. 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',
      ];
      
  3. 請舉一段不符合 PSR-2 的程式碼,並改寫成符合規範的版本。

    • 不符合 PSR-2 的程式碼 (常見的 K&R 或自訂風格):

      PHP
      class 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、括號位置、空格、縮排):

      PHP
      class UserModel
      {
          public function getData($id, $format = 'json')
          {
              if ($id <= 0) {
                  throw new \Exception('Invalid ID');
              }
      
              $this->data = $this->db->getRecord($id);
          }
      }
      

🧱 PSR-3:日誌介面

架構與擴充性

  1. PSR-3 定義了哪些 log level?你在什麼情境下會使用 alert 而不是 critical

    • Log Level (嚴重性由高到低):

      1. emergency

      2. alert

      3. critical

      4. error

      5. warning

      6. notice

      7. info

      8. debug

    • alert vs. critical 的情境區分:

      • critical (系統關鍵錯誤): 指的是應用程式本身的錯誤,通常是無法預期需要立即關注的故障,但系統的核心功能可能仍在運行

        • 情境範例: 資料庫連線中斷、一個主要的微服務超時、程式碼拋出致命的 Exception 導致單次請求失敗。

      • alert (緊急行動警報): 指的是必須立即採取行動,因為它影響了整個系統的可用性或重要性。它比 critical 更具外部緊急性

        • 情境範例: 整個網站/服務下線、支付閘道完全無法響應、監控系統發現主硬碟已滿 (disk full)、備份失敗。

        • 記憶方法: alert 是給 Ops 團隊的,critical 是給 Dev 團隊的alert 應該觸發即時的 Pager/SMS 警報。

  2. 你如何在 Laravel 或 Symfony 中整合 PSR-3 相容的 logger?請說明具體做法。

    • Laravel:

      • 做法: Laravel 預設的 Log 服務是透過 Monolog 實現的,而 Monolog 完全符合 PSR-3 規範。因此,Laravel 的 Log Facade 或透過 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]);
            }
        }
        
  3. 如果你要替換 Monolog 為自製 logger,如何確保相容 PSR-3?請寫出介面實作的範例。

    • 確保相容性: 只要自製的 Logger 實作 \Psr\Log\LoggerInterface 介面,並遵循介面中定義的八個日誌方法 (emergencydebug) 以及核心的 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:自動載入標準

架構與維護性

  1. 請說明 PSR-4 的命名空間與目錄結構對應原則。與 PSR-0 有何差異?

    • PSR-4 原則 (推薦標準):

      1. 定義一個命名空間前綴 (Namespace Prefix)

      2. 這個前綴必須對應到一個基礎目錄 (Base Directory)

      3. 命名空間前綴之後的子命名空間,必須精確對應到基礎目錄之後的子目錄。

      4. 類別名稱 (Class Name) 必須對應到以 .php 結尾的檔案名稱。

    • PSR-4 與 PSR-0 的差異 (關鍵):

      | 特性 | PSR-4 (Current) | PSR-0 (Deprecated) |

      | :--- | :--- | :--- |

      | 命名空間前綴 | 必須對應到基礎目錄。 | 可以對應到任何目錄。 |

      | Vendor 前綴 | 不要求必須在檔案結構中體現。 | 必須包含 Vendor Name,且 Vendor Name 必須對應到實體目錄。 |

      | 下底線 (_) | 不處理。通常不建議用於命名空間或類別名。 | 會被視為目錄分隔符。 |

      | 核心優勢 | 簡化了檔案結構,移除不必要的 Vendor 或 Root 命名空間目錄層級,提高了自動載入的效率。 | 檔案結構可能過深,且對 Vendor/子目錄的處理較為僵化。 |

  2. 你如何在 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.phpvendor/composer/autoload_psr4.php 等檔案,建立命名空間到檔案路徑的對應表。

  3. 如果你有一個命名空間 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:

      1. 手動驗證 (Class Exist): 在專案的啟動檔案中 (index.phppublic/index.php),載入 Composer 的 autoload 檔案,然後檢查類別是否存在。

        PHP
        require 'vendor/autoload.php';
        // 檢查類別是否存在,如果不存在,Autoloading 就失敗了
        if (class_exists(\App\Services\Payment\StripeGateway::class)) {
            echo "Autoload 成功!";
        } else {
            echo "Autoload 失敗!";
        }
        
      2. CLI 驗證 (Composer Validate/Diagnose): 使用 Composer 內建工具檢查設定。

        Bash
        composer validate  # 檢查 composer.json 語法及設定是否正確
        
      3. 使用測試 (Unit Test): 這是最可靠的方式。如果一個 Unit Test 需要載入這個類別才能執行,而測試通過,就表示 Autoloading 正確。


🔍 加分題:整合與實務挑戰

  1. 你如何在 CI/CD 流程中自動檢查 PSR-1~4 的遵守情況?用哪些工具?

    • 我會使用 PHP_CodeSniffer (PHPCS)PHP-CS-Fixer 這兩個業界標準工具。

    • 流程:

      1. 設定階段:

        • 安裝工具:composer require --dev squizlabs/php_codesniffer friendsofphp/php-cs-fixer

        • 設定 PHPCS: 建立一個 phpcs.xml 或使用 CLI 配置,指定使用 PSR12 規則集(PSR-12 包含並取代了 PSR-1PSR-2)。同時,PSR-4 的規則檢查也包含在內。

      2. 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

      3. Hooks (加分): 搭配 GrumPHPPre-Commit Hook,在開發者 git commit 之前執行 Linter,提早發現並修正錯誤。

  2. 你曾經在大型專案中推動 PSR 標準嗎?遇到哪些阻力?如何解決?

    • 經歷: 是的,我在一個從舊版 PHP 升級到 PHP 7+ 的大型專案中推動過 PSR-1/2/4 (後續升級為 PSR-12) 標準化。

    • 遇到的阻力:

      1. 歷史遺留問題 (最大的阻力): 專案中數十萬行的程式碼使用了 K&R 風格、Tabs 縮排,並且許多類別不符合 PSR-4 命名規範。一次性修改成本高昂且風險極大。

      2. 資深開發者習慣: 某些資深工程師對自己的風格有強烈的偏好,認為 PSR 規範「囉嗦」、「不實用」,抗拒改變手寫習慣。

      3. 新舊程式碼衝突: 在推動過程中,新舊程式碼風格不一致,導致視覺混亂,反而影響了可讀性。

    • 解決方案:

      1. 分階段實施:

        • 遺留程式碼 (Legacy Code): 暫時排除風格檢查,或只檢查最核心的 PSR-1/4 (命名規範)。

        • 新程式碼 (New Code): 在 CI/CD 中設定硬性規則:所有新的或被修改的檔案必須通過 PSR-12 檢查。

      2. 自動化工具導入:

        • 強制使用 PHP-CS-Fixer: 不依賴人工記憶,要求所有人在 Commit 前執行風格修正指令。最好將其整合到 Git Hook 或 IDE 的「儲存時自動格式化」功能。

        • 單次大規模重構: 對於少部分核心且穩定的遺留程式碼,在專門的排程時間進行一次性大規模的 php-cs-fixer 跑遍,然後進行嚴格的迴歸測試。

      3. 教育與溝通: 強調 PSR 的價值在於**「降低認知負擔」「提升團隊效率」**,而不是美學或個人偏好。透過 Code Review 示範不一致風格帶來的閱讀成本,讓團隊成員體會到統一標準的好處。

  3. 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 標準化運動的全面理解。

沒有留言:

張貼留言

📦 LogiFlow WMS:打造 SaaS 多租戶倉儲管理系統的技術實踐

📦 LogiFlow WMS:打造 SaaS 多租戶倉儲管理系統的技術實踐 在企業數位化的浪潮下,倉儲管理系統 (WMS) 不再只是單一公司的內部工具,而是需要支援 多租戶 (Multi-Tenant) 的 SaaS 架構。這意味著系統必須在共享基礎設施的同時,保有嚴格的資...