PHP Magic Methods
剛接觸PHP對於 Magic Methods感到驚艷,因為這增加了程式碼撰寫的彈性,尤其是 __get
和 __set
讓properties的編寫容易。同時,也可以針對Object lifecycle 做一些加值的工作。但是,這也是兩面刃,使用不當unserialize
會造成安全性的問題 - PHP Object Injection。
注意:PHP 規定連續兩個 underline __
functions保留給 magic methods。 所以在命名上,自訂 functions 請勿使用__
以免混淆。
PHP Magic Methods 分類
PHP 目前有15個 magic methods,依照功能大致分類如下:
Categories | Magic Methods |
---|---|
Stringification (物件字串輸出輸入) | __toString, __set_state, __debugInfo |
Lifecycle (物件生命週期) | __construct, __destruct |
Property Overloading (屬性覆寫) | __set, __get, __isset, __unset |
Method Overloading (方法覆寫) | __call, __callStatic |
Serialization (序列化) | __sleep, __wakeup |
Cloning (複製) | __clone |
Object Invocation (物件呼叫) | __invoke |
Stringification 物件字串
這裡要注意的是個別的Functions對應到不同的 Magic Methods:
PHP Methods | Magic Methods |
---|---|
echo, print | __toString |
var_export | __set_state |
var_dump | __debugInfo |
注意: 使用 __debugInfo,會覆寫原本的var_dump();
注意: 如果沒有 __toString,當你 echo $obj會得到fatal error.
範例:
<?php
namespace DemoMagic;
class CustomerBase
{
public $name;
public $address;
public function __construct($name, $address)
{
$this->name = $name;
$this->address = $address;
}
}
class DemoDebugInfoCustomer extends CustomerBase
{
public function __debugInfo():array
{
return [
'userInfo' => 'name: ' . $this->name . ' address: ' . $this->address
];
}
}
class DemoToStringCustomer extends CustomerBase
{
public function __toString():string
{
return 'name: ' . $this->name . ' address: ' . $this->address;
}
}
$base = new CustomerBase('Young', 'Taipei');
var_dump($base);
$demoDebugInfo = new DemoDebugInfoCustomer('Young', 'Taipei');
var_dump($demoDebugInfo);
$demoDemoToStringCustomer = new DemoToStringCustomer('Young', 'Taipei');
echo $demoDemoToStringCustomer;
//echo $base; // fatal error;
Property Overloading 屬性複寫
PHP是一個很彈性的語言,何時使用 public properties, getters and setters, or magic methods? 每個寫法都有優點和缺點,並沒有絕對的好或壞。
Public Properties
<?php
namespace DemoMagic;
class Foo
{
public $bar;
}
$foo = new Foo();
$foo->bar = 1;
$foo->bar++;
$json = json_encode($foo);
echo $json . "\n";
$obj = json_decode($json);
echo $obj->bar . "\n";
- 優點
- 可以少寫很多的代碼
- 程式容易閱讀
- public properties 的物件可以 serialize by json_encode
- 缺點
- property 的所存放的資料型別或數值沒有辦法控制,如果別人使用的時候,不了解你內部的機制,很容易造成錯誤。
Getters and Setters
<?php
namespace DemoMagic;
class Cust
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
}
class Foo
{
private $bar;
private $customer;
public function getBar():int
{
return $this->bar;
}
public function setBar(int $bar)
{
if ($bar < 0) {
throw new \InvalidArgumentException('only accept equal or greater zeor integer.');
}
$this->bar = $bar;
}
public function setCust(Cust $cust)
{
$this->customer = $cust;
}
}
$foo = new Foo();
$foo->setBar(1);
$foo->setBar($foo->getBar() + 1);
echo 'bar: ', $foo->getBar(), "\n";
$foo->setCust(new Cust('Young'));
var_dump($foo);
try {
$foo->setBar(-1);
} catch (\Exception $e) {
echo 'Exception: ', $e->getMessage(), "\n";
}
try {
$foo->setBar('this is a string');
} catch (\Error $e) {
echo 'Error: ', $e->getMessage(), "\n";
}
- 優點
- 可以檢查property的型別
- 可以在 Setter / Getter 內做自訂的處理。例如上面的範例int和大於零的檢查。
- 可以封裝內部private變數的宣告
- 缺點
- 不能使用
++
這樣便捷的寫法,必須要$foo->setBar($foo->getBar() + 1);
- 增加了額外許多 set and get 開頭的 functions
- 相對於 public properties 降低了程式碼的閱讀性。
- 不能使用
Magic method getters and setters example
<?php
namespace DemoMagic;
class Foo2
{
public function __get($property)
{
if (property_exists($this, $property)) {
return $this->$property;
} else {
trigger_error('Undefined property via __get(): ' . $property, E_USER_NOTICE);
return null;
}
}
public function __set($property, $value)
{
// dynamic create property
$this->$property = $value;
}
}
$foo = new Foo2();
$foo->bar = 1;
$foo->ccc = 2;
$foo->bar++;
var_dump($foo);
優點
- 在上面的程式碼中,我用到了PHP語言神奇的特性,可以動態地給 class 增加 propterties。通才在底層利用class的方式,動態地將資料庫的欄位和資料,轉換成PHP物件。
可以在set和get的functions中,做property的型別判斷或是額外的加工。例如:
public function __set($property, $value) { switch ($property) { case 'square': $this->squre = pow($value, 2); break; //etc. } }
- 可以決定哪些屬性只能 read or write only
- 將程式碼有關屬性的設定都集中在固定的地方。
- 可以用簡單的運算方式如
++
缺點
- 程式碼需要用phpDoc的方式將屬性說明
動態屬性意味著在執行階段,才可以知道這物件會有哪些屬性。需要處理當外界使用這物件拿到不存在的屬性的處理。也要讓使用這物件的client端增加 try catch 的error handle。建議在 get 內一定要增加下面程式碼的判斷。
if (property_exists($this, $property)) { return $this->$property; }
__isset and __unset
<?php
namespace DemoMagic;
class Foo2
{
private $inaccessible;
public function __get($property)
{
if (property_exists($this, $property)) {
return $this->$property;
} else {
trigger_error('Undefined property via __get(): ' . $property, E_USER_NOTICE);
return null;
}
}
public function __set($property, $value)
{
// dynamic create property
$this->$property = $value;
}
// triggered by calling isset() or empty() on inaccessible properties.
public function __isset($property)
{
echo 'issetting: ', $property, "\n";
return property_exists($this, $property) && !is_null($this->$property);
}
// is invoked when unset() is used on inaccessible properties.
public function __unset($property)
{
echo 'unsetting: ', $property, "\n";
unset($this->$property);
}
}
$foo = new Foo2();
// dynamic create properties
$foo->bar = 1;
$foo->ccc = 2;
$foo->bar++;
echo '$foo->bar is visible property, isset will not go into magic __set', "\n";
var_dump(isset($foo->bar) ? 'true' : 'false');
var_dump($foo);
echo 'use property_exists to check private properties', "\n";
echo property_exists($foo, 'inaccessible') ? 'true' : 'false', "\n";
var_dump(isset($foo->inaccessible));
$foo->inaccessible = 10;
echo $foo->inaccessible . "\n";
unset($foo->inaccessible);
var_dump($foo);
在 php 中我們常常會呼叫 isset 檢查某個變數是否已經存在記憶體中,而且值不是 NULL。 當 isset 檢查 inaccessible 屬性時,會呼叫到 __isset magic methods,讓我們在 class scope 內可以檢查其值。
另外,請通常我們不會實作 __unset 這個 function,意味著可以透過外面的方式,將內部的 private 屬性給刪除。 上面的實做只是作為 Demo。可以看見 inaccessible 的變數,被dealloc了。