今天把 Coding Style/PSR 跟 OOP/SOLID 合併成一天。這不是為了省篇幅隨便湊——這兩個主題本來就是同一件事的不同層次:PSR 講的是「程式碼長什麼樣子」的表層風格,OOP/SOLID 講的是「程式碼怎麼組織」的結構性原則,兩者合起來才是「乾淨的程式碼」完整的樣貌。這塊沒有版本差異,是可以純粹談觀念的一天。
程式碼被讀的次數遠比被寫的次數多。風格一致的程式碼降低團隊協作的認知負擔、減少不必要的 code review 來回、也讓工具(IDE、靜態分析)能更好地輔助開發。
Laravel 官方 contribution guide 目前文字上寫的是「遵循 PSR-2 coding standard、PSR-4 autoloading standard」,這句話從很早期的版本就沒改過。但這裡有個重要的實務現況要知道:PSR-2 本身已經在 2019 年被官方棄用,由 PSR-12 取代,PSR-12 後來又被 PER Coding Style 接手維護以跟上 PHP 語言的新特性。
那 Laravel 專案到底該遵循哪個?答案是:不用自己摳規則細節,交給 Laravel Pint 處理。Pint 是 Laravel 官方的程式碼風格自動修正工具,每個新專案都內建:
./vendor/bin/pint # 修正所有檔案
./vendor/bin/pint --dirty # 只修正尚未提交的變更(適合 commit 前跑一次)
./vendor/bin/pint --test # 只檢查、不修改,適合放進 CI
Pint 底層可以選擇多種 preset(laravel、per、psr12、symfony 等),預設用的是 Laravel 自家風格(精神上貼近 PER Coding Style/PSR-12)。實務上你不需要背熟 PSR-2 或 PSR-12 條文細節,寫完程式碼跑一次 pint,剩下的交給工具。
如果團隊對某些規則有特定偏好(例如強制陣列語法一定要用短語法 [] 而不是 array()),可以在專案根目錄放一個 pint.json:
{
"preset": "laravel",
"rules": {
"array_syntax": { "syntax": "short" },
"no_unused_imports": true,
"ordered_imports": { "sort_algorithm": "alpha" }
}
}
pint.json 一旦提交進版本控制,團隊每個人(以及 CI 流程)跑 pint 時都會套用同一套規則,不需要各自在 IDE 設定裡各自調整——這也是為什麼 Day 3 建議的 VS Code 設定裡加了「儲存時自動跑 Pint」,讓風格規則的執行完全自動化,不依賴人為記得手動執行。
Pint 處理的是「風格」(程式碼長什麼樣子),但不會告訴你「這段程式碼邏輯上有沒有問題」(例如呼叫了一個可能不存在的方法、型別不匹配)。這是靜態分析工具要解決的問題,Laravel 生態圈最常見的選擇是 Larastan(PHPStan 的 Laravel 專用擴充):
composer require --dev larastan/larastan
./vendor/bin/phpstan analyse
Larastan 能在執行程式碼之前就抓出很多類型的錯誤,例如呼叫了 Model 上不存在的屬性、方法回傳型別跟宣告不符。它有分析等級(level 0 到 9,數字越大檢查越嚴格),新專案建議從較低等級開始,逐步拉高,不要一開始就設定成最嚴格等級——那樣通常會產生大量一時難以處理的警告,反而讓團隊失去導入的意願。
Laravel 生態圈的命名慣例大致統一:
| 對象 | 命名法 | 範例 |
|---|---|---|
| 變數、方法 | 小駝峰(camelCase) | $publishedAt、getPost() |
| 類別、Enum | 大駝峰(PascalCase) | PostController、PostStatus |
| 資料表、欄位 | 蛇式(snake_case),資料表用複數 | posts、published_at |
| 路由 URI | 中線(kebab-case) | /posts/{post}/toggle-featured |
變數命名本身也是一種溝通——$p、$data 這種命名雖然打字快,但半年後回頭看(或別人接手)幾乎等於重新猜一次這個變數是什麼;$post、$publishedPosts 這種具名變數,讓程式碼本身就是文件。
// 不好的註解:只是把程式碼翻譯成中文,沒有額外資訊
// 把 is_featured 設成 true
$post->is_featured = true;
// 好的註解:解釋「為什麼」,程式碼本身看不出來的脈絡
// 手動精選文章需要繞過自動推薦演算法的權重計算,因此直接設定旗標
$post->is_featured = true;
public function getPublishedPosts(): Collection
{
return Post::whereNotNull('published_at')->get();
}
public function findPost(int $id): ?Post
{
return Post::find($id);
}
明確標註回傳型別(Collection、?Post 這種可為 null 的型別),除了讓 IDE 能提供更準確的自動完成,也讓呼叫端不需要看方法內容就知道要處理什麼型別,Day 9 建立關聯方法時已經在用這個習慣(: BelongsTo、: HasMany)。
PHP 8.1+ 引入的 readonly 屬性也值得養成使用習慣,特別適合 Day 18 的自訂例外、Day 17 表達「這個值一旦建構就不該再變」的場景:
class InvalidPostStateException extends Exception
{
public function __construct(
public readonly int $postId, // 建構後任何地方想改這個屬性,會直接在編譯期被擋下來
string $message = '文章目前狀態不允許此操作',
) {
parent::__construct($message);
}
}
跟只在文件裡寫「這個屬性不該被修改」相比,readonly 讓這個約束變成語言本身會強制執行的規則——這是型別系統能替你在寫程式的當下就抓到錯誤的又一個具體例子(跟 Day 11 提到的 Enum 轉型是同一個精神)。
物件導向的四大特性,Laravel 框架本身的設計就是現成的範例:
把內部實作細節藏起來,只透過明確定義的介面互動:
class Post extends Model
{
protected $fillable = ['title', 'body']; // 只有這些屬性能被外部批量賦值
public function publish(): void
{
$this->published_at = now();
$this->save();
}
}
// 外部程式碼不需要知道「發布」內部是設定哪個欄位、要不要額外處理
$post->publish();
PostController extends Controller、Post extends Model——這系列從 Day 9 開始寫的每個 Model、Controller,本身就是繼承的實際應用,子類別自動獲得父類別的能力(Model 提供的 save()/find() 等),再加上自己的專屬邏輯。
同一個介面,不同實作可以有不同行為。Laravel 的 Route Model Binding(Day 13)就是一種多型的應用:不管你綁定的是 Post、User 還是任何其他 Model,Laravel 都用同一套機制處理「依 ID 查詢並注入」,不需要為每種 Model 寫一套專屬邏輯。
定義「該做什麼」而不是「怎麼做」,透過 Interface 表達:
interface NotificationChannel
{
public function send(string $message, User $recipient): void;
}
class EmailNotificationChannel implements NotificationChannel
{
public function send(string $message, User $recipient): void
{
Mail::to($recipient)->send(new GenericNotification($message));
}
}
class SlackNotificationChannel implements NotificationChannel
{
public function send(string $message, User $recipient): void
{
// 呼叫 Slack API...
}
}
呼叫端只需要依賴 NotificationChannel 這個抽象介面,不需要知道背後究竟是 Email 還是 Slack 在處理——這也是 Day 23 Service Container 會深入的「依賴反轉」的基礎。

物件導向教學常強調繼承,但實務上很多時候「組合」(一個類別擁有另一個類別的實例、把工作委派給它)比繼承更靈活。假設你一開始這樣設計:
// 不理想:用繼承表達「這個 Post 是可加精選的」
class FeaturablePost extends Post
{
public function feature(): void
{
$this->update(['is_featured' => true]);
}
}
問題是「加精選」這個能力如果之後也想套用在 Comment(精選留言)上,繼承完全幫不上忙——你不能讓 Comment 也繼承 FeaturablePost(Comment 又不是一種 Post)。改用組合:
// 更理想:獨立出一個負責處理的類別,Post/Comment 各自持有它、委派工作給它
class FeaturableManager
{
public function feature(Model $model): void
{
$model->update(['is_featured' => true]);
}
}
或者用 PHP 的 Trait 達到類似效果(讓多個不相關的類別共享同一段行為,而不需要共同的繼承鏈):
trait Featurable
{
public function feature(): void
{
$this->update(['is_featured' => true]);
}
}
class Post extends Model
{
use Featurable;
}
判斷原則:「這個類別是不是另一個類別的一種」用繼承(Admin extends User 合理,因為管理員本質上就是一種使用者);「這個類別需要另一個類別提供的能力,但彼此本質上是不同的東西」用組合或 Trait。當你發現繼承鏈越疊越深、子類別只為了改一兩個方法而存在時,這通常是該考慮改用組合的訊號。
一個類別只該有一個改變的理由。PostController 只負責處理 HTTP 請求/回應,不該同時塞進「怎麼寄通知信」「怎麼算文章熱門分數」這些邏輯——這正是 Day 19 把通知邏輯抽成獨立 Job、Day 23 會介紹的 Service 類別存在的理由。
一個違反 SRP 的反例:
// 不理想:一個方法同時處理驗證、寫入資料庫、寄信、記錄日誌四件事
public function store(Request $request)
{
$validated = $request->validate([...]);
$post = Post::create($validated);
Mail::to($post->author->subscribers)->send(new PostPublishedMail($post));
Log::info('文章已建立', ['post_id' => $post->id]);
return redirect()->route('posts.show', $post);
}
這個方法「改變的理由」至少有四個:驗證規則變了要改、儲存邏輯變了要改、通知方式變了要改、日誌格式變了要改。拆開之後(用 Day 17 的 Form Request 處理驗證、Day 19 的 Job 處理通知、Day 23 的 Service 處理業務邏輯),store() 方法會回歸到它真正該做的事:協調各個部分,而不是自己動手做所有事情。
對擴充開放、對修改封閉。前面 NotificationChannel 的例子:想加一個新的通知管道(例如 LINE Notify),只需要新增一個實作類別,不需要修改既有的 EmailNotificationChannel/SlackNotificationChannel。
子類別必須能夠替換父類別使用,不改變程式的正確性。如果 PremiumUser extends User 但覆寫了 posts() 方法讓行為完全不符合 hasMany 關聯的預期(例如回傳型別不一致),任何原本預期操作 User 的程式碼換成 PremiumUser 就可能出錯,這違反了 LSP。
不要強迫類別實作它用不到的方法。與其定義一個包山包海的 Payable 介面(charge()、refund()、subscribe()、cancelSubscription()……),拆成 Chargeable、Refundable、Subscribable 幾個小介面,讓每個類別只實作它真正需要的部分。
高階模組不該依賴低階模組的具體實作,兩者都該依賴抽象。延續前面的例子,PostController 依賴 NotificationChannel 這個介面,而不是直接依賴 EmailNotificationChannel 這個具體類別——Day 23 的 Service Container 綁定機制,就是 Laravel 落實 DIP 的核心工具。
blog-app 這種規模的專案,不需要每個功能都刻意拆出 Interface + 多個實作類別,SOLID 是「當複雜度需要時」的工具,不是無論如何都要套用的規則,Day 23 會再討論這個取捨。今天把程式碼風格(PSR、命名、註解、型別宣告、readonly)跟程式碼結構(OOP 四大特性、組合優於繼承、SOLID 五大原則)合併整理,也補上了 Pint 客製化設定跟 Larastan 靜態分析這兩個實務上能大幅提升程式碼品質的工具。這兩塊在 Laravel 10 到 13 之間相當穩定,唯一值得更新認知的是「PSR 版本焦慮交給 Pint 處理」這個實務現況。
不以規矩,不能成方圓 — 《孟子・離婁上》
Day 23 把 SOLID 的依賴反轉原則落地:Service Container 怎麼運作、Service Provider 怎麼組織,最後用 blog-app 文章列表的 Service + Repository 分層架構,把今天的原則變成能跑的程式碼。