說說 PHP 的魔術方法及其應用

麥索發表於2017-04-11

PHP中將所有__(兩個下劃線)開頭的類方法作為魔術方法,這方法之所以稱為魔術方法是因為其實現的功能就如變魔術一樣感覺很神奇。在特定的事件下觸發,這真的很酷。

__construct()

這個方法應該是最常用的,被稱為構造器或者構造方法,當一個物件被例項化時會被首先呼叫,而在 PHP 框架中一些過濾器,中介軟體及依賴注入也一般在這個方法中完成。父類的構造器可以被子類繼承和重寫。

<?php
class A {

    public function __construct() {
        echo "This is A construct\n";
    }
}

class B extends A{

    // 呼叫父類構造方法,再呼叫自己的構造方法
    public function __construct() {
        parent::__construct();
        echo "This is B construct\n";
    }
}

class C extends A{

    // 重寫構造方法,之呼叫自己的構造方法
    public function __construct() {
        echo "This is C construct";
    }
}

new A();// This is A construct
new B();// This is A construct This is B construct
new C();// This is c construct

以上示例程式碼將按順序輸出:

This is A construct
This is A construct
This is B construct
This is C construct

構造方法能幫助我們完成一些資料初始化,屬性初始化的任務,在例項化類後使得呼叫類更便利。

__destruct()

析構方法,PHP 將物件銷燬前將呼叫這個方法,這個方法可能對於 PHP 這種執行時間短的指令碼可能無意義,但在有些情況下還是具有意義的。

比如你需要一個長時間執行的指令碼,設定 set_time_limit(0); 後需要不斷執行這個指令碼,一般這樣的指令碼是迴圈執行一些任務,這其中可能會涉及到頻繁的建立某個物件,這時候析構方法就會起到作用,它可以將物件開啟的一些資源及時的釋放,以防止記憶體溢位或單個程式佔用過多記憶體。

<?php

class Log{

  public function __construct() {
    $this->created = time();
    $this->logfile_handle = fopen('/tmp/log.txt', 'w');
  }

  public function __destruct() {
    fclose($this->logfile_handle);
  }
}

__get()與__set()

這兩個方法的作用是當呼叫或設定一個類及其父類方法中未定義的屬性時這個方法會被觸發。

<?php 

class MethodTest
{
    private $data = array();

    public function __set($name, $value){
        $this->data[$name] = $value;
    }

    public function __get($name){
        if(array_key_exists($name, $this->data))
            return $this->data[$name];
        return NULL;
    }

}

class Penguin extends Animal {

  public function __construct($id) {
    $this->getPenguinFromDb($id);
  }

  public function getPenguinFromDb($id) {
    // elegant and robust database code goes here
  }

  public function __get($field) {
    if($field == 'name') {
        return $this->username;
    }
  }

  public function __set($field, $value) {
     if($field == 'name') {
        $this->username = $value;
     }
  }

}

在 MethodTest 這個類中使用 get 和 set 將所有不存在的屬性都儲存在類的 data 屬性中,而在Penguin 類中我們連線了資料庫或者是資料提供者,由於某些原因資料來源中原來的 name 變更為 username ,如果這時要檢查所有呼叫 Penguin 類的地方將 name 換成 username 顯然是困難而且無趣的甚至會有忽略的地方,而使用一個 __get 方法我們不用改變外部呼叫的屬性名就可以實現從 name 轉變為 username

__call 和 __callStatic

call 和 callStatic 是類似的方法,前者是呼叫類不存在的方法時執行,而後者是呼叫類不存在的靜態方式方法時執行。正常情況下如果呼叫一個類不存在的方法 PHP 會丟擲致命錯誤,而使用這兩個魔術方法我們可以替換一些更友好的提示或者記錄錯誤呼叫日誌資訊、將使用者重定向、丟擲異常等等,亦或者是如同set 和 get 那樣做方法的重新命名。

class A
{

    public static function __callStatic($name, $arguments)
    {   
        var_dump($name);
        var_dump($arguments);
        echo 'unknown static method ' . $name;
    }

    function __call($name, $arguments)
    {
        var_dump($name);
        var_dump($arguments);
        echo 'unknown method ' . $name;
    }
}

$a = new A();
$a->agfdgdrsfgdf([123,3213]);
A::sdfsd();

__sleep() 和 __wakeup()

當我們執行 serialize()unserialize() 對物件進行操作是時,會呼叫這兩個方法,比如物件有一個資料庫連結,想要在反序列化時恢復連結狀態,而在序列化時希望將屬性鍵名儲存就可以使用這兩個魔術方法:

<?php
class Connection 
{
    protected $link;
    private $server, $username, $password, $db;

    public function __construct($server, $username, $password, $db)
    {
        $this->server = $server;
        $this->username = $username;
        $this->password = $password;
        $this->db = $db;
        $this->connect();
    }

    private function connect()
    {
        $this->link = mysql_connect($this->server, $this->username, $this->password);
        mysql_select_db($this->db, $this->link);
    }

    public function __sleep()
    {
        return array('server', 'username', 'password', 'db');
    }

    public function __wakeup()
    {
        $this->connect();
    }
}

__clone()

如同名字一樣,這個方法在物件被複制是呼叫,如我們要實現一個單例模式,我們可以用這個魔術方法防止物件被克隆。

<?php 
public class Singleton {
    private static $_instance = NULL;

    // 私有構造方法 
    private function __construct() {}

    public static function getInstance() {
        if (is_null(self::$_instance)) {
            self::$_instance = new Singleton();
        }
        return self::$_instance;
    }

    // 防止克隆例項
    public function __clone(){
        die('Clone is not allowed.' . E_USER_ERROR);
    }
}

__toString()

當物件被當做字串是呼叫此方法。

PHP 5.2.0 之前,toString() 方法只有在直接使用於 echo 或 print 時才能生效。PHP 5.2.0 之後,則可以在任何字串環境生效(例如透過 printf(),使用 %s 修飾符),但不能用於非字串環境(如使用 %d 修飾符)。自 PHP 5.2.0 起,如果將一個未定義 toString() 方法的物件轉換為字串,會產生 E_RECOVERABLE_ERROR 級別的錯誤。

// Declare a simple class
class TestClass
{
    public function __toString() {
        return 'this is a object';
    }
}

class Penguin {

  public function __construct($name) {
      $this->species = 'Penguin';
      $this->name = $name;
  }

  public function __toString() {
      return $this->name . " (" . $this->species . ")\n";
  }
}

$class = new TestClass();
echo $class;

$tux = new Penguin('tux');
echo $tux;

在 TestClass 的呼叫中我們輸出了一個友好的提示,而在 Penguin 我們將物件的屬性組合後輸出,比如在模板中呼叫。

__invoke()

當嘗試用函式的方式呼叫一個物件是觸發此方法。

PHP 5.3.0 新增

<?php
class CallableClass 
{
    function __invoke($x) {
        var_dump($x);
    }
}
$obj = new CallableClass;
$obj(5); // int(5)
var_dump(is_callable($obj)) // bool(true)

__set_state()

呼叫 var_export() 匯出類時,此魔術方法被呼叫。

PHP 5.1.0 新增

<?php
class A
{
    public $var1;
    public $var2;

    public static function __set_state ($an_array) {
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';
var_dump(var_export($a));

__debuginfo()

這個方法在對物件使用 var_dump() 時呼叫。

PHP 5.6.0 新增

<?php
class C {
    private $prop;

    public function __construct($val) {
        $this->prop = $val;
    }

    public function __debugInfo() {
        return [
            'propSquared' => $this->prop ** 2,
        ];
    }
}

var_dump(new C(42));
/*
object(C)#1 (1) {
  ["propSquared"]=>
  int(1764)
}
*/
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章