最近更新: 2007-01-30

活用 PHP5 的 magic methods - __set(), __get() and __call()

PHP5 在動態性及個體導向兩方面都做了大幅度的加強。其中 Magic methods 概念的引用,更為 PHP5 帶來許多靈活性。

本文說明如何活用 Magic methods 重整 (refactoring) 程式碼。讓 PHP 的程式碼更易於使用。主要重點在 __set(), __get() ,同時也示範了 __toString(), __isset(), __call() 的用途。

首先看一個 PHP4 語法的類別。See also: PHP Manual - Classes and Objects (PHP 4)

X.php in PHP4

<?php
class X {
    var $_value1;
    var $_value2;

    function X4() {
        $this->_value1 = 0;
        $this->_value2 = 0;
    }

    function setValue1($v) {
        $this->_value1 = $v;
    }
    function getValue1() {
        return $this->_value1;
    }

    function setValue2($v) {
        $this->_value2 = $v;
    }
    function getValue2() {
        return $this->_value2;
    }
}

$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";
?>

第一步應該要先將它改成 PHP5 語法。See also: PHP Manual - Classes and Objects (PHP 5)

在重整過程中,除了語法的改寫外,也會改變私有資料成員 $_value1, $_value2 的結構,改以一個陣列放置所有私有屬性。此處先說明一下本文用語。本文通常稱呼用於表示個體內部資訊、狀態的資料項目為「屬性 (property)」。如果一個「屬性」直接指派給一個變數成員時,即 $this->value1 的形式,則本文稱這個「屬性」同時也是一個「資料成員 (data member)」。另一方面,也可用一個容器 (陣列) 作為「資料成員」,而將所有或大部份的「屬性」存放在這個「資料成員」的容器中。此時,本文就不稱呼這個「屬性」為一個「資料成員」。例如:陣列 $this->propertyMap 是一個「資料成員」,而 $this->propertyMap['value1'] 是一個「屬性」;在本文的用語中,此時的 value1 是一個屬性,但不是一個資料成員。底下是重整後的 X.php 。

X.php in PHP5

<?php
class X {
    protected $_ = array(
        'value1' => 0,
        'value2' => 0
    );

    public function __construct() {
    }

    public function setValue1($v) {
        $this->_['value1'] = $v;
    }
    public function getValue1() {
        return $this->_['value1'];
    }

    public function setValue2($v) {
        $this->_['value2'] = $v;
    }
    public function getValue2() {
        return $this->_['value2'];
    }
}

$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";
?>

為何重整後改以關聯陣列的資料成員儲存私有屬性,而不直接用資料成員表示私有屬性呢?這跟 PHP 的「自識」能力 (或稱「反射」, Reflection) 有關。雖然關聯陣列和資料成員都可以動態增加內容,但受限於 PHP 的自識能力,使用關聯陣列儲存私有屬性較資料成員方便。因為使用資料成員的形式時無法以 foreach 走訪所有私有屬性。若用關聯陣列為容器存放屬性,便可以 foreach 走訪所有私有屬性。稍候的重整會顯示此用法的意義。

第二步,為了方便展示使用 magic methods 前後差異,自 X 類別衍生一個新類別 X2 ,在 X2 類別實作 magic methods 的內容。

X2.php

<?php
require_once 'X.php';

class X2 extends X {
    public function __construct() {
        parent::__construct();
    }
    public function __toString() {
        $s = '(';
        foreach ($this->_ as $v) {
            $s .= $v . ',';
        }
        $s = substr($s, 0, strrpos($s, ',')) . ')';
        return $s;
    }
    public function __set($k, $v) {
        $method = 'set' . ucfirst($k);
        if (method_exists($this, $method))
            $this->$method($v);
        else if (isset($this->_[$k]))
            $this->_[$k] = $v;
        else
            throw new Exception('property undefined!');
    }
    public function __get($k) {
        $method = 'get' . ucfirst($k);
        if (method_exists($this, $method))
            return $this->$method();
        else if (isset($this->_[$k]))
            return $this->_[$k];
        else
            throw new Exception('property undefined!');
    }
    public function __isset($k) {
        return isset($this->_[$k]);
    }
}

$x = new X;
$x->setValue1(1);
$x->setValue2(2);
echo '(' . $x->getValue1() . ',' . $x->getValue2() . ')', "\n";

$x2 = new X2;
$x2->value1 = 1;
$x2->value2 = 2;
echo $x2->value1, "\n";
echo 'isset $x2->value2: ', (isset($x2->value2) ? 'true' : 'false'), "\n";
echo $x2, "\n";
?>

我們覆載 __set(), __get() 這兩個 magic methods 之後,表面上我們直接存取資料成員 $x2->value1, $x2->value2 ,但實際上根本沒有這兩個資料成員,而是藉由 __set() 去調用屬性 value1, value2 的專屬存取行為 $this->setValue1(), $this->setValue2()(X2.php 第19行)。我們用 method_exists() 判斷是否定義了專屬存取行為,用 變數函數 (variable function) 的形式調用。同樣地,雖然實際上沒有 $x2->value1, $x2->value2 資料成員,但表面上有,因此又覆載了 __isset() 以判斷是否設定了屬性 value1, value2 。最後可以看到 __toString() 的覆載作用。而在 __toString() 中也可以看出用關聯陣列存放屬性時,我們可以很方便地使用 foreach 走訪所有屬性。

接下來的重整目標是函數名稱的命名方式。其實在 PHP 中,我們不需用 setValue1()/getValue1() 的命名方式。利用預設參數就可以將 getter 和 setter 寫在一起,直接用 value1() 就可以表示兩者。而且用這種命名方式時, X2.php 第17行的 $method = 'set' . ucfirst($k); 之動作便可省略。 __set()/__get() 的處理效率會更好。

繼續自 X2 類別衍生一個新類別 X3 ,利用預設參數撰寫新的屬性存取行為。同時再利用 __call() 增加對函數名稱的相容性。

X3.php

<?php
require_once 'X2.php';

class X3 extends X2 {
    public function __construct() {
        parent::__construct();
        $this->_['value3'] = 1;
    }

    public function value3($v = NULL) {
        if ($v === NULL)
            return $this->_['value3']; // be a getter
        else
            $this->_['value3'] += $v*2; // be a setter
    }

    public function __set($k, $v) {
        if (method_exists($this, $k))
            $this->$k($v);
        else if (isset($this->_[$k]))
            $this->_[$k] = $v;
        else
            throw new Exception('property undefined!');
    }
    public function __get($k) {
        if (method_exists($this, $k))
            return $this->$k();
        else if (isset($this->_[$k]))
            return $this->_[$k];
        else
            throw new Exception('property undefined!');
    }
    public function __call($k, $args) {
        if (preg_match_all('/(set|get)([A-Z][a-z0-9]+)+/', $k, $words)) {
            $firstWord = $words[1][0]; // set or get
            $methodName = strtolower(array_shift($words[2]));
            //first word of property name

            foreach ($words[2] as $word) {
                $methodName .= ucfirst($word);
            }
            if (method_exists($this, $methodName)) {
                $methodObj = array(&$this, $methodName);
                if ($firstWord == 'set') {
                    call_user_func_array($methodObj, $args);
                    return;
                }
                else {
                    return call_user_func_array($methodObj, NULL);
                }
            }
        }
        throw new Exception('property undefined!');
    }
}

$x3 = new X3;
$x3->value1 = 1;
$x3->value2 = 2;
$x3->value3 = 3;
echo $x3->value3(), "\n";
echo $x3->getValue3(), "\n";
$x3->value3(2);
$x3->setValue3(2);
echo $x3, "\n";

try {
    $x3->value4 = 0;
}
catch (Exception $e) {
    echo $e;
}
?>

X3.php 的第62行 (echo $x3->getValue3();) 與第64行 ($x3->setValue3(2);) ,都是透過 __call() 的覆載,才能調用到正確的 $x3->value3() (call_user_func_array())。不用擔心這會降低程式效能,因為 PHP5 只有在找不到明確定義的函數行為時,才會調用 __call() 。因此只要統一使用一致的函數名稱,便沒有機會調用 __call() 了。在本例中先後用了不一致的函數命名方式,才利用 __call() 容錯。

本文以繼承方式,利用 magic methods 逐一重整程式碼。實務上則不妨直接重整原有類別,無需繼承。透過 __set()/__get() ,除了將語法從函數調用修飾為成員存取,也提供我們幾乎不用增加任何函數行為就可以增加新的屬性,並以預設的方式保有對屬性的保護與控制之能力。應善用之。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/2683180.html