2018年12月16日

認識PHP魔術方法: __call(應用篇)

Introduce to php magic method: __call (Application)

本篇為PHP系列

上一篇:認識PHP魔術方法: __call

前言

在上一篇文章中,我們說明了 PHP 魔術方法:__call(),還不熟悉或想要複習的朋友可以閱讀上一篇,而本篇會接著討論 __call() 的一些應用方式,給大家參考。

應用

1.當類別中的每個方法都需要先執行固定的步驟。

例如:

執行每個動作之前都要進行資料庫連線,執行動作結束後都要斷開資料庫的連線。

class TodoList
{
 public function add($newTodo)
 {
  $this->connectDB();

  //process add...

  $this->disconnectDB();  
 }

 public function remove($todoId)
 {
  $this->connectDB();

  //process remove...

  $this->disconnectDB();  
 }
}

這樣的情況,就可以透過 __call() 來簡化程式:

class TodoList
{
 public function __call($method, $arguments)
 {
  $this->connectDB();
  
  call_user_func_array(array($this, $method), $arguments);

  $this->disconnectDB();  
 }

 private function add($newTodo)
 {
  //process add...
 }

 private function remove($todoId)
 {
  //process remove...
 }
}

流程說明:

  1. 主程式呼叫到 add 方法,因 add 為 private 無法被外部直接執行,所以觸發了 __call。
  2. 在 __call 中,執行 $this->connectDB(),連結資料庫。
  3. 透過 call_user_func_array 呼叫外界原本實際想要執行的方法,例如 add。
  4. 執行完畢後,呼叫 $this->disconnectDB(),與資料庫斷線。

當主程式呼叫 remove 時,也會進行一樣的流程。

透過 __call,開發人員就可以不必在每個類別方法中,都處理資料庫的連線問題了!

2.用一個類別來代理另外一個類別

比如說我們有一個 Notepad 類別,這個類別有 TodoList 的功能,

一般情況下我們會把一個 TodoList 的實體放到這個類別中,讓程式透過 getter 來取用 TodoList 物件。

例如:

$notepad = new Notepad();
$notepad->getTodoList()->add('to reserve a restaurant');

這是第一種方法,

而這個方法的缺點是,沒辦法在呼叫 TodoList 類別的方法之前或之後, 同時進行一些 Notepad 類別所需的處理,例如同時更新 Notepad 的顯示。

為了做到這件事,我們可以使用第二種方法,在 Notepad 中實作 add 方法,對外部隱藏 TodoList 物件的存在,外部只呼叫 Nodepad 的 add:

class Notepad
{
 public function add($newTodo)
 {
  // can do some process for Notepad here. 
 
  $this->todoList->add($newTodo);
 }
}
$notepad = new Notepad();
$notepad->add('to reserve a restaurant');

第二種方式解決了第一種方法的缺點,但當 TodoList 的方法變多時,有些方法對 Notepad 來說只是單純的往下呼叫 TodoList 類別的方法而已,卻還要為此新增一個方法來處理這件事,相當麻煩。

這種時候我們就可以試著用 __call 可以來同時解決這兩種方式的缺點:

class TodoList
{
 public function add($newTodo) {//do something }
 public function remove($todoId) {//do something }
}

class Notepad
{
 public function __construct()
 {
  $this->todoList = new TodoList();
 } 

 public function __call($name, $arguments)
 {
  if (method_exists($this->todoList, $name)) {
   call_user_func_array(array($this->todoList, $name), $arguments);
  } else {
   echo "hey! method: {$name} is not exist!\n";
  }
 }

 public function remove($todoId)
 {
  // do some thing here...

  $this->todoList->remove($todoId);
 }
}

$notepad = new Notepad();
$notepad->add('to reserve a restaurant');
$notepad->remove(1);

說明:

以此例來說,$notepad 呼叫了 add(),而 add 在 Notepad 類別中並沒有實作,因此會觸發 __call,而在 __call 中,它會透過 call_user_func_array 幫你呼叫 TodoList 中的 add()。

這解決了第二種方式的缺點,我們不必要為每個 TodoList 類別的方法,特別在NotePad中新增ㄧ個方法。

而第二步 $notepad 呼叫了 remove(),因為在 Notepad 類別中有實作 remove(),因此會呼叫 Notepad 類別的 remove(),等做完一些事情後,才會去呼叫 TodoList 類別的 remove()。這解決了第一種方式的缺點,讓 Notepad 可以先做完一些事情,才呼叫 TodoList 類別的 remove()。

使用這樣的技巧,我們可以不必為了讓 Notepad 可以使用每個 TodoList 的方法,而特別在 Notepad 中個別新增對應的 TodoList 方法,並且也可以在必要的時候,在 Notepad 中先做一些處理,才呼叫 TodoList 的方法。

以這樣的方式使用 __call 達到了讓 Notepad 代理了 TodoList 效果,並且維持了一些擴充性。

結論

善用 __call() 可以達到一些有趣的效果,其實許多當代的PHP框架中也利用了 __call() 的技巧來達到一些簡潔的行為,有興趣可以直接研究這些框架的 source code,學習前人怎麼巧妙地使用__call()。


本篇程式碼執行環境

OS: Mac OS Mojave 10.14.20

PHP: PHP 7.3.0

上一篇:認識PHP魔術方法: __call

看更多PHP系列