iT邦幫忙

DAY 9
10

逐步提昇PHP技術能力系列 第 9

逐步提昇PHP技術能力 - PHP的語言特性 : magic methods

magic methods是一系列以__開頭的方法名稱,如果在類別中定義了這些方法,系統會在特定的時機呼叫。在PHP4,類別的constructor是跟類別同名的方法,到PHP5,則改名為__construct()這個magic method。

magic methods的設計,就類似Template Method Pattern,只要類別定義了某個method,系統就會在特定的時機呼叫。例如類別實例化時會呼叫__construct()方法,存取屬性時如果屬性不存在會嘗試呼叫__get()/__set(),呼叫類別方法如果方法不存在時會嘗試呼叫__call()等等。
參考:PHP: Magic Methods - Manual

先來看一下幾個之前介紹過的Magic Methods

* __construct()

這也就是類別的constructor,會在類別實例化時被呼叫,可以在其中初始化物件。

* __destruct()

這也就是類別的destructor,當物件要被從系統中「消滅」時,會呼叫這個方法。至於物件何時被消滅?當然系統結束時會被消滅,另外,我在介紹型別時有稍微提到zval,他有一個屬性是被參考數。當這個屬性歸零時,就代表物件沒人使用了,這時系統會做gc,物件就被「消滅」了。

* __call / __callStatic

這是PHP實作method overload的方式,如果呼叫物件的某方法,而這個方法沒有在類別中定義的話,系統會嘗試呼叫__call()。實作__call()然後過濾系統傳入的方法名稱,就可以讓物件表現的像是有定義這個方法。__callStatic()也是一樣的作用,只是是針對靜態方法。

* __set() / __get() / __isset() / __unset()

PHP透過這幾個方法實現屬性的overload。讀取物件屬性時,如果屬性不存在,系統會嘗試呼叫__get()。如果類別有實作這個方法,就可以過濾傳入的屬性名稱,看看是否要返回值。__set則是對應到寫入物件屬性的狀況。另外,PHP可以透過isset()函數檢查物件屬性是否已設定、unset()函數來讓物件屬性回到未設定(null)的狀態,這時如果物件屬性不存在,則會嘗試呼叫__isset()及__unset()。

再來看一下其他的mgaic methods:

* __wakeup() / __sleep()

如果有在類別中定義的話,這兩個magic methods會在物件序列化/反序列化時被呼叫。

如果類別有定義這個方法,__sleep()就會在物件開始序列化前被呼叫。他會返回一個陣列,裡面列舉需要被序列化的屬性名稱。這樣在進行序列化時,系統會針對這些屬性來進行操作。一個簡單的例子:

<?php
class a {
  public $name;
  public function __sleep() {
    $this->name = 'fillano';
    return array("name");
  }
}
$a = new a;
$str = serialize($a);
$b = unserialize($str);
var_dump($b);

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8b.php 
object(a)#2 (1) {
  ["name"]=>
  string(7) "fillano"
}

可以看到,類別a實例化時,並沒有初始化屬性name,但是因為在__sleep()中設定屬性name的值為fillano,然後回傳屬性名稱陣列。結果在反序列化後,物件實例的name屬性的值就變成fillano了。

__wakeup()方法則是在反序列化後會立刻被呼叫,所以上例可以改成在__wakeup()中設定name屬性的值,結果是一樣的。

* __toString()

如果定義了這個方法並且回傳一個字串,那把物件當做字串操作時,系統會呼叫__toString()來取得代表物件的字串。例如:

<?php
class hello {
  public function __toString() {
    return "Hello ";
  }
}
class world {
  public function __toString() {
    return "World.\n";
  }
}
$a = new hello;
$b = new world;
echo $a.$b;

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8c.php 
Hello World.

* __invoke()

這是PHP5.3才有的magic method。當物件被當做函數來呼叫時,就會呼叫這個方法。可以用is_callable()函數來檢查物件是否可以當做函數執行,可以的話會回傳true。例如:

<?php
class a {
  public function __invoke() {
    return __CLASS__." is invoked.\n";
  }
}
$a = new a;
if(is_callable($a)) {
  echo $a();
}

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8d.php
a is invoked.

* __set_state()

這個從PHP5.1加入的magic method是一個靜態方法,所以在定義時需要使用static關鍵字。PHP有一個var_export()函數,可以輸出變數的結構化資訊字串,這個資訊同時也是合法的PHP程式碼,所以可以被eval()執行。

如果類別定義了這個靜態方法,當使用var_export()來處理物件實例時,系統會先檢查這個方法是否存在,然後產生呼叫這個靜態方法的程式碼字串,在程式碼中,會把物件實例的屬性陣列當做參數傳遞給他。看一下程式範例以及執行結果會比較清楚:

<?php
class A {
  public $name;
  public $address;
  public static function __set_state($p) {
    $obj = new B;
    foreach($p as $k=>$v) {
      $obj->data .= "$k:$v;";
    }
    return $obj;
  }
}
class B {
  public $data;
}
$a = new A;
$a->name = 'fillano';
$a->address = 'taipei';
var_export($a);
echo "\n";
eval('$b='.var_export($a, true).';');
var_dump($b);

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8e.php
A::__set_state(array(
   'name' => 'fillano',
   'address' => 'taipei',
))
object(B)#2 (1) {
  ["data"]=>
  string(28) "name:fillano;address:taipei;"
}

可以看到,呼叫var_export()後,輸出了一個呼叫A::__set_state()的程式碼字串。傳給var_export()的第二個參數是true的話,他會返回這個字串。所以就把它在eval組裝起來,把結果指派給$b。

說實話,這個功能還蠻詭異的XD

* __clone()

在PHP程式中,可以用這樣的方式clone一個物件:

<?php
class A {}
$a = new A;
$b = clone $a;

這時$b是一個新的class A的實例,只是$a與$b的屬性值都一樣。__clone()會在複製完畢時對新的物件執行,所以可以在需要時,調整複製後的物件屬性。例如做一個簡單複製計數:

<?php
class A {
  public $count=1;
  public function __clone() {
    $this->count++;
  }
}
$a = new A;
var_dump($a);
$b = clone $a;
var_dump($b);
var_dump($a===$b);

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8f.php 
object(A)#1 (1) {
  ["count"]=>
  int(1)
}
object(A)#2 (1) {
  ["count"]=>
  int(2)
}
bool(false)

物件的屬性如果是物件的話,系統並不會自動額外clone一份,所以會參考到同一個物件。例如:

<?php
class A {
  public $obj;
  public function __construct() {
    $this->obj = new B;
  }
}

class B {
  public $name='fillano';
}

$a = new A;
$b = clone $a;
$b->obj->name = 'james';
var_dump($a->obj->name);

執行結果:

eng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8g.php 
string(5) "james"

因為$a與$b的obj屬性其實參考到同一個物件,所以改了$b->obj就會影響到$a->obj。要避免這個狀況發生,就需要在__clone()中,針對A::obj也做一次clone:

<?php
class A {
  public $obj;
  public function __construct() {
    $this->obj = new B;
  }
  public function __clone() {
    $this->obj = clone $this->obj;
  }
}

class B {
  public $name='fillano';
}

$a = new A;
$b = clone $a;
$b->obj->name = 'james';
var_dump($a->obj->name);

執行結果:

Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-8h.php
string(7) "fillano"

這樣針對clone後物件的操作,就不會影響到被clone的物件。


上一篇
逐步提昇PHP技術能力 - PHP的語言特性 : 型別 / Type Juggling / Type Hint
下一篇
逐步提昇PHP技術能力 - Convention 與 include/require
系列文
逐步提昇PHP技術能力30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
SunAllen
iT邦研究生 1 級 ‧ 2013-10-09 10:25:58

讚

費大的文很棒,可是能力不夠的我,還不知道該怎麼使用...落寞

fillano iT邦超人 1 級 ‧ 2013-10-09 10:56:36 檢舉

需要的時候再回頭看看就可以啦,這些東西太眉眉角角XD

我要留言

立即登入留言