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.
https://gyazo.com/cbf6e32ea3faa0d23dc15e6ae1c07e28

範例:

<?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;

Output result:
https://gyazo.com/c38fa6c2829217a7a622e76576845919

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";
}

https://gyazo.com/d114f4d4b6414148476db05ecae37c90

  • 優點
    • 可以檢查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了。

https://gyazo.com/a9d8f004871c030fd3f4384ee18a3e84

Reference

results matching ""

    No results matching ""