PHP 類別裡的東西,不是每一樣都要對外開放。把所有屬性都設成 public,就像把家裡的抽屜全部裝上透明玻璃:找東西很方便,但誰都能順手改設定。這篇會用白話帶你分清楚 publicprivateprotected 的差異,並知道什麼時候該讓外部存取、什麼時候該把實作細節關在類別裡。

先理解:存取修飾符在管什麼?

存取修飾符(Access Modifier)用來指定類別的屬性與方法「哪些程式碼可以使用」。PHP 提供三種常見的可見性:

  • public:任何地方都可以存取。
  • protected:宣告它的類別,以及該類別的子類別可以存取。
  • private:只有宣告它的類別本身可以存取。

這裡的「可以存取」包含讀取屬性、修改屬性,以及呼叫方法。修飾符不會讓資料自動加密,也不會替你擋住所有錯誤輸入;它主要是在類別邊界上畫線,讓程式碼知道哪些是公開介面、哪些是內部實作。

簡單記法是:public 對外開門、protected 留給繼承家族、private 自己收藏。這三扇門沒有誰天生比較高級,重點是門後面放了什麼。

public:公開給外部使用

標記為 public 的屬性或方法,可以在類別外直接使用。通常,這適合放「使用這個類別的人真的需要知道」的功能,例如建立訂單、計算總額或取得狀態。

<?php

class CoffeeOrder
{
	public string $size = 'medium';

	public function summary(): string
	{
		return "一杯 {$this->size} 咖啡";
	}
}

$order = new CoffeeOrder();
$order->size = 'large';

echo $order->summary(); // 一杯 large 咖啡

外部可以直接修改 $size,也可以呼叫 summary()。程式碼短、上手快,適合確定不需要額外驗證的簡單資料。不過,公開屬性等於把修改權也交出去;如果未來要限制尺寸只能是 smallmediumlarge,就會發現大家早已習慣直接改它。

所以,public 的問題通常不是「不能用」,而是「公開之後,這個介面就可能變成別人依賴的契約」。一旦使用者很多,改它就像改咖啡店招牌,總有人會在門口問:「為什麼昨天還是這個名字?」

private:把實作細節留在類別內

private 成員只能在宣告它的類別內使用。類別外不能直接讀取,子類別也不能直接存取同一個私有成員。

這很適合用來保護物件的內部狀態,並把修改規則集中在方法裡。以下例子用 private 保存年齡,外部只能透過公開方法設定:

<?php

class Person
{
	private int $age = 0;

	public function setAge(int $age): void
	{
		if ($age < 0) {
			throw new InvalidArgumentException('年齡不能是負數');
		}

		$this->age = $age;
	}

	public function getAge(): int
	{
		return $this->age;
	}
}

$person = new Person();
$person->setAge(25);

echo $person->getAge(); // 25

這裡的重點不是多寫了 getAge()setAge(),而是所有修改年齡的規則都集中在類別內。未來若要加入上限、記錄異動,或改成其他驗證方式,外部呼叫程式不必跟著知道內部怎麼做。

也因此,當你不確定某個屬性是否應該公開時,先從 private 開始通常比較穩妥。需要對外提供功能時,再設計一個清楚的 public 方法。封裝不是把資料藏起來炫技,而是避免每個呼叫者都各自發明一套修改規則。

protected:讓子類別共享

protected 成員不能從類別外直接存取,但宣告它的類別與子類別可以使用。它常出現在有繼承關係的基礎類別裡,讓子類別共用某些內部工具或狀態。

<?php

class Report
{
	protected function formatTitle(string $title): string
	{
		return "【報表】{$title}";
	}
}

class SalesReport extends Report
{
	public function render(): string
	{
		return $this->formatTitle('本月銷售');
	}
}

$report = new SalesReport();

echo $report->render(); // 【報表】本月銷售
// $report->formatTitle('測試'); // 錯誤:類別外不能直接呼叫 protected 方法

SalesReport 可以使用父類別的 protected 方法,但外部只能呼叫它公開的 render()。這讓父類別保留一部分可供擴充的工具,同時不把工具本身變成對外 API。

不過,protected 不是「比 private 更慷慨就一定更好」。它會把父類別的內部細節暴露給所有子類別;當繼承層次變深,修改這些成員可能牽動許多地方。如果某個方法只屬於父類別自己的實作,使用 private 往往更安全;只有確定子類別需要延伸它時,才考慮 protected

三種修飾符怎麼選?

可以先用下面這個問題快速判斷:

  1. 這是使用者需要呼叫的功能嗎?是的話,考慮 public 方法。
  2. 這是類別內部的資料或輔助方法嗎?是的話,優先考慮 private
  3. 這是子類別明確需要重用或覆寫的行為嗎?是的話,才考慮 protected

實務上,通常會讓屬性保持 privateprotected,再透過 public 方法提供有意義的操作。這樣外部看到的是「可以做什麼」,而不是「裡面每一格資料目前長什麼樣」。

一張表快速比較

修飾符類別內子類別類別外
public可以可以可以
protected可以可以不可以
private可以不可以直接使用父類別的成員不可以

另外,子類別可以用相容的可見性覆寫父類別方法,但不能把繼承而來的公開方法改得更不公開。例如,不能把 public 覆寫成 protected。這個限制是為了維持父類別對外承諾的介面;不然呼叫者才剛拿到門票,子類別就把入口縮成貓洞,介面會先崩潰。

封裝不等於「全部藏起來」

封裝(Encapsulation)的目的,是把資料與操作資料的規則放在一起,並控制外部能依賴的介面。它不是要求每個成員都必須是 private,也不是寫越多 getter、setter 就越物件導向。

例如,與其提供一個可以任意修改餘額的公開屬性:

<?php

class Wallet
{
	private int $balance = 0;

	public function deposit(int $amount): void
	{
		if ($amount <= 0) {
			throw new InvalidArgumentException('存入金額必須大於零');
		}

		$this->balance += $amount;
	}

	public function balance(): int
	{
		return $this->balance;
	}
}

外部只能透過 deposit() 增加餘額,不能直接把餘額改成負數。這種公開的是「行為」,隱藏的是「狀態」;通常比單純公開一個欄位更能守住規則。

結語

publicprotectedprivate 的差異,可以濃縮成一句話:先決定誰需要知道,再決定誰可以使用。

把真正要給外部呼叫的功能設為 public,把內部實作優先留在 private,只有在繼承設計確實需要時才使用 protected。當類別的邊界清楚,程式碼會比較容易修改,也比較不會因為一個小改動引發連鎖反應。接下來閱讀 PHP 的繼承與介面時,你會更容易看懂每扇門到底是為誰開的。

參考資料