PHP 允許父類呼叫子類定義的方法,這是「模板方法模式」的核心實現方式。本文透過實例解析父類調用子類方法的機制、應用場景與注意事項。
<?php
/**
* 基礎應用類 - 父類
*/
class BaseApp {
/**
* 模板方法:定義演算法骨架,延遲具體步驟到子類
*/
protected function _run_action() {
$action = 'index'; // 預設動作
// 關鍵:呼叫當前實例 (子類) 的方法
$this->$action();
}
}
/**
* 預設應用類 - 子類
*/
class DefaultApp extends BaseApp {
/**
* 子類實作的具體動作
* 將被父類的 _run_action() 呼叫
*/
public function index() {
echo "DefaultApp->index() invoked\n";
}
/**
* 公開入口:觸發父類模板方法
*/
public function go() {
// 呼叫父類方法,但實際執行子類的 index()
parent::_run_action();
}
}
// 執行
$app = new DefaultApp();
$app->go(); // 輸出: DefaultApp->index() invoked
| 概念 | 說明 |
|---|---|
| 多態 | $this->$action() 在執行時綁定到實際物件類型 (子類) |
| 晚期靜態綁定 | PHP 5.3+ static::method() 解決靜態方法繼承問題 |
| 實例化主體 | 必須實例化子類,父類抽象不可直接實例化 |
| 可見性 | 子類方法至少 protected,父類才能存取 |
<?php
abstract class DataProcessor {
// === 模板方法 (final 防止覆寫) ===
final public function process($source) {
$data = $this->read($source); // 步驟 1: 讀取 (子類實作)
$data = $this->transform($data); // 步驟 2: 轉換 (子類實作)
$this->write($data); // 步驟 3: 寫入 (子類實作)
$this->postProcess(); // 步驟 4: 後處理 (可選,預設空)
}
// === 抽象方法:強制子類實作 ===
abstract protected function read($source);
abstract protected function transform(array $data);
abstract protected function write(array $data);
// === 鉤子方法:子類可選覆寫 ===
protected function postProcess() {
// 預設不做事
}
}
// CSV 處理器
class CsvProcessor extends DataProcessor {
protected function read($filepath) {
$rows = [];
if (($fp = fopen($filepath, 'r')) !== false) {
while (($row = fgetcsv($fp)) !== false) {
$rows[] = $row;
}
fclose($fp);
}
return $rows;
}
protected function transform(array $data) {
// 第一行作為標題
$header = array_shift($data);
return array_map(function($row) use ($header) {
return array_combine($header, $row);
}, $data);
}
protected function write(array $data) {
// 這裡可存入資料庫、API、檔案等
echo "寫入 " . count($data) . " 筆資料\n";
}
protected function postProcess() {
echo "CSV 處理完成,發送通知郵件...\n";
}
}
// JSON 處理器
class JsonProcessor extends DataProcessor {
protected function read($filepath) {
return json_decode(file_get_contents($filepath), true);
}
protected function transform(array $data) {
// 統一欄位命名慣例
return array_map(function($item) {
return array_change_key_case($item, CASE_LOWER);
}, $data);
}
protected function write(array $data) {
echo "JSON 匯入 " . count($data) . " 筆完成\n";
}
}
// 使用
$csv = new CsvProcessor();
$csv->process('data.csv');
$json = new JsonProcessor();
$json->process('data.json');
<?php
abstract class ActiveRecord {
protected $table;
protected $primary_key = 'id';
protected $fillable = [];
// 模板方法:儲存 (新增/更新)
public function save() {
$this->beforeSave();
if ($this->{$this->primary_key}) {
$this->update();
} else {
$this->insert();
}
$this->afterSave();
}
// 子類必須實作:取得資料表欄位定義
abstract protected function getSchema();
// 鉤子方法
protected function beforeSave() {}
protected function afterSave() {}
private function insert() {
$fields = array_intersect_key($this->attributes, array_flip($this->fillable));
// 執行 INSERT...
$this->{$this->primary_key} = $this->db->lastInsertId();
}
private function update() {
// 執行 UPDATE...
}
}
class User extends ActiveRecord {
protected $table = 'users';
protected $fillable = ['name', 'email', 'password'];
protected function getSchema() {
return [
'id' => 'int(11) AI PK',
'name' => 'varchar(100)',
'email' => 'varchar(150) UNIQUE',
'password' => 'varchar(255)',
];
}
protected function beforeSave() {
if (isset($this->attributes['password'])) {
$this->attributes['password'] = password_hash($this->attributes['password'], PASSWORD_DEFAULT);
}
}
}
$user = new User();
$user->name = 'John';
$user->email = 'john@example.com';
$user->password = 'secret123';
$user->save(); // 自動哈希密碼並插入
| 誤區 | 正確理解 |
|---|---|
| 「父類呼叫子類」違反依賴倒置 | 這是「好萊塢原則」:Don't call us, we'll call you。父類定義流程,子類填充細節 |
| 直接實例化父類也能呼叫子類方法 | 錯誤。必須實例化子類,$this 才指向子類實例 |
| 靜態方法也能這樣用 | 靜態方法需用 static::method() (LSB) 而非 self::method() |
| 所有方法都應可被父類呼叫 | 僅將「演算法步驟」暴露給父類,內部輔助方法保持 private |
<?php
#[\Attribute]
class Step {
public function __construct(public int $order) {}
}
class Processor {
public function execute() {
$methods = (new ReflectionClass($this))->getMethods();
$steps = [];
foreach ($methods as $method) {
$attrs = $method->getAttributes(Step::class);
if ($attrs) {
$steps[$attrs[0]->newInstance()->order] = $method->name;
}
}
ksort($steps);
foreach ($steps as $method) {
$this->$method();
}
}
}
class MyProcessor extends Processor {
#[Step(10)] protected function stepOne() { echo "Step 1\n"; }
#[Step(20)] protected function stepTwo() { echo "Step 2\n"; }
#[Step(30)] protected function stepThree() { echo "Step 3\n"; }
}
(new MyProcessor())->execute();
// 輸出: Step 1 → Step 2 → Step 3 (順序由屬性決定,不需父類硬編碼)
父類呼叫子類方法不是「反模式」,而是模板方法、策略模式、工廠模式等經典設計模式的基石。關鍵在於:父類定義「做什麼」(流程),子類決定「怎麼做」(實作)。