PHP 5 引入物件導向可見性修飾符,為資料封裝提供語言級保障。合理使用 public、protected、private,能防止外部誤用內部狀態、降低耦合、提升可維護性。本文結合資料庫連線、查詢物件等實戰場景,示範可見性的具體應用模式。
| 修飾符 | 存取範圍 | 典型用途 |
|---|---|---|
public | 任意位置 | 對外 API、需被廣泛存取的屬性/方法 |
protected | 本類 + 子類 | 內部實現細節、供子類覆寫/擴展的方法 |
private | 僅本類 | 核心狀態、不希望被繼承體系觸碰的邏輯 |
模擬預處理語句與參數綁定,展示如何用可見性保護內部資源:
<?php
interface DatabaseConnection
{
public function query(string $sql, array $params = []): ResultSet;
public function prepare(string $sql): PreparedStatement;
}
class MySqlConnection implements DatabaseConnection
{
// 私有:連線資源,絕不對外暴露
private $link = null;
// 受保護:配置參數,子類(如主從分離版)可覆寫
protected $config = [
'host' => 'localhost',
'dbname' => 'app',
'charset' => 'utf8mb4'
];
public function __construct(array $config = [])
{
$this->config = array_merge($this->config, $config);
$this->connect();
}
// 私有:建立連線細節,外部不可呼叫
private function connect(): void
{
$dsn = "mysql:host={$this->config['host']};dbname={$this->config['dbname']};charset={$this->config['charset']}";
$this->link = new PDO($dsn, $this->config['user'], $this->config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
// 公開:符合介面契約
public function query(string $sql, array $params = []): ResultSet
{
$stmt = $this->link->prepare($sql);
$stmt->execute($params);
return new ResultSet($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function prepare(string $sql): PreparedStatement
{
return new PreparedStatement($this->link->prepare($sql));
}
}
/**
* 查詢物件:封裝單一 SQL 與參數,支援預處理複用
*/
class PreparedStatement
{
// 私有:PDOStatement 實例,防止外部任意操作
private $stmt;
// 私有:已綁定參數快取
private $boundParams = [];
public function __construct(PDOStatement $stmt)
{
$this->stmt = $stmt;
}
// 公開:綁定參數(支援鏈式呼叫)
public function bind(string $param, $value, int $type = PDO::PARAM_STR): self
{
$this->boundParams[$param] = [$value, $type];
return $this;
}
// 公開:執行並回傳結果集
public function execute(): ResultSet
{
foreach ($this->boundParams as $param => [$value, $type]) {
$this->stmt->bindValue($param, $value, $type);
}
$this->stmt->execute();
return new ResultSet($this->stmt->fetchAll(PDO::FETCH_ASSOC));
}
// 公開:編譯後的 SQL(除錯用,不實際執行)
public function compile(): string
{
$sql = $this->stmt->queryString;
foreach ($this->boundParams as $param => [$value]) {
$sql = str_replace($param, is_string($value) ? "'{$value}'" : $value, $sql);
}
return $sql;
}
}
/**
* 結果集:不可變資料容器
*/
class ResultSet implements IteratorAggregate, Countable
{
private array $rows;
private int $position = 0;
public function __construct(array $rows)
{
$this->rows = $rows;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->rows);
}
public function count(): int
{
return count($this->rows);
}
// 僅提供讀取介面,禁止修改內部陣列
public function first(): ?array
{
return $this->rows[0] ?? null;
}
public function all(): array
{
return $this->rows;
}
}
<?php
$db = new MySqlConnection([
'user' => 'app_user',
'pass' => 'secret',
]);
// 1. 直接查詢
$users = $db->query('SELECT * FROM users WHERE status = ?', ['active']);
foreach ($users as $user) {
echo $user['name'] . "\n";
}
// 2. 預處理複用(批次插入)
$insert = $db->prepare('INSERT INTO logs (level, message) VALUES (:level, :msg)');
$insert->bind(':level', 'info')->bind(':msg', 'User login')->execute();
$insert->bind(':level', 'warn')->bind(':msg', 'Slow query')->execute();
// 3. 編譯檢視(除錯)
echo $insert->compile(); // INSERT INTO logs (level, message) VALUES ('warn', 'Slow query')
// 4. 嘗試存取私有屬性 → Fatal Error
// $db->link->exec('DROP TABLE users'); // 錯誤:Cannot access private property
private,僅在確需繼承覆寫時改 protected,對外暴露才用 public。interface 定義,實現類內部細節全隱藏。ResultSet)僅提供讀取方法,內部陣列 private,防止外部竄改。$user->changeEmail($new) 而非 $user->setEmail($new))。| 錯誤模式 | 問題 | 修正 |
|---|---|---|
所有屬性 public | 破壞封裝,外部任意修改狀態 | 改 private + 提供精確行為方法 |
為了測試改 protected | 洩露實現細節,子類誤用風險 | 用 Reflection 或測試專用 Trait |
| 子類需用父類私有方法 | 複製代碼或違反封裝 | 提取為 protected 方法或獨立服務類 |
| 大量 Getter/Setter | 貧血模型,業務邏輯外泄 | 封裝為領域行為(如 placeOrder()) |
可見性修飾符不是形式,而是架構意圖的語言表達。嚴守封裝邊界,系統才能在演進中保持穩健。