2015年12月14日 星期一

PHP的魔術方法(magic methods)

會寫下這篇原因是 上海逸尚信息咨询 曾經考過這題

magic methods是一系列以__開頭的方法名稱,如果在類別中定義了這些方法,系統會在特定的時機呼叫。

 __construct
在PHP4時,Class的建構函數( constructor )是跟類別同名的方法,到PHP5,則改名為__construct()這個magic method。
在Class new成 Object時呼叫

__destruct()
解構函數( destructor ),當物件要被從系統中「消滅」時,會呼叫這個方法。

__call / __callStatic
這是PHP實作method overload的方式,如果呼叫物件的某方法,而這個方法沒有在類別中定義的話,系統會嘗試呼叫__call()。實作__call()然後過濾系統傳入的方法名稱,就可以讓物件表現的像是有定義這個方法。__callStatic()也是一樣的作用,只是是針對靜態方法。
例:
class a {
    function __call($name, $args) {
        echo $name . " : " . print_r($args, true) . "\n";
    }
}

$a = new a;
$a->func1('abc', 'def');

class b {
    function __call($name, $args) {
        echo "\n__call:";
        switch ($name) {
        case 'add':
            if (count($args) === 2) {
                if (is_numeric($args[0]) && is_numeric($args[1])) {
                    return $args[0] + $args[1];
                }

                if (is_string($args[0]) && is_string($args[1])) {
                    return $args[0] . $args[1];
                }

            }
        default:
            throw new Exception("[warning] b::$name method not found.\n");
        }
    }
    public static function __callStatic($name, $args){
        echo "\n__callStatic:";
        switch ($name) {
            case 'add':
                if (count($args) === 2) {
                    if (is_numeric($args[0]) && is_numeric($args[1])) {
                        return $args[0] + $args[1];
                    }

                    if (is_string($args[0]) && is_string($args[1])) {
                        return $args[0] . $args[1];
                    }

                }
                break;
            
            default:
                throw new Exception("[warning] b::$name method not found.\n");
                break;
        }
    }
}

$b = new b;
echo $b->add(2, 3) . "\n";
echo $b->add('hello', ' world.') . "\n";
try {
    echo $b->add(2, ' world.') . "\n";
} catch (Exception $e) {
    echo $e->getMessage();
}

echo $b::add(2, 3) . "\n";
echo $b::add('hello', ' world.') . "\n";
結果:
func1 : Array
(
    [0] => abc
    [1] => def
)


__call:5

__call:hello world.

__call:[warning] b::add method not found.

__callStatic:5

__callStatic:hello world.
注意:
function __callStatic() 必須是 public static ,否則會跳Warning:
PHP Warning:  The magic method __callStatic() must have public visibility and be static in magic_methods/method_overloading.php on line 30
上面例子如果$b->add()的兩個參數不同時為數字或字串,ex. $b->add(2, ' world.'); 。 則會報錯

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

    }
}

$a = new a;
echo $a->var1 . "\n";
echo $a->var2 . "\n";

class b {
    private $var1 = 'abc';
    private $var2 = 'def';
    function __get($name) {
        switch ($name) {
        case 'var1':
            return $this->var1;
            break;
        case 'var2':
            return 'ghi';
            break;
        }
    }
}

$b = new b;
echo $b->var1 . "\n";
echo $b->var2 . "\n";

class c {}

$c = new c;
echo $c->var1 . "\n";
結果:
abc

abc
ghi
PHP Notice:  Undefined property: c::$var1 in /cygdrive/e/www/test/magic_methods/attribute_overloading.php on line 37
__set() / __get() / __isset() / __unset()所有例子:
class PropertyTest {
    /**  被重载的数据保存在此  */
    private $data = array();

    /**  重载不能被用在已经定义的属性  */
    public $declared = 1;

    /**  只有从类外部访问这个属性时,重载才会发生 */
    private $hidden = 2;

    public function __set($name, $value) {
        echo "Setting '$name' to '$value'\n";
        $this->data[$name] = $value;
    }

    public function __get($name) {
        echo "Getting '$name'\n";
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }

        $trace = debug_backtrace();
        trigger_error(
            'Undefined property via __get(): ' . $name .
            ' in ' . $trace[0]['file'] .
            ' on line ' . $trace[0]['line'],
            E_USER_NOTICE);
        return null;
    }

    /**  PHP 5.1.0之后版本 */
    public function __isset($name) {
        echo "Is '$name' set?\n";
        return isset($this->data[$name]);
    }

    /**  PHP 5.1.0之后版本 */
    public function __unset($name) {
        echo "Unsetting '$name'\n";
        unset($this->data[$name]);
    }

    /**  非魔术方法  */
    public function getHidden() {
        return $this->hidden;
    }
}

echo "\n";

$obj = new PropertyTest;

$obj->a = 1;
echo $obj->a . "\n\n";

var_dump(isset($obj->a));
unset($obj->a);
var_dump(isset($obj->a));
echo "\n";

echo $obj->declared . "\n\n";

echo "Let's experiment with the private property named 'hidden':\n";
echo "Privates are visible inside the class, so __get() not used...\n";
echo $obj->getHidden() . "\n";
echo "Privates not visible outside of class, so __get() is used...\n";
echo $obj->hidden . "\n";
結果:
Setting 'a' to '1'
Getting 'a'
1

Is 'a' set?
bool(true)
Unsetting 'a'
Is 'a' set?
bool(false)

1

Let's experiment with the private property named 'hidden':
Privates are visible inside the class, so __get() not used...
2
Privates not visible outside of class, so __get() is used...
Getting 'hidden'
PHP Notice:  Undefined property via __get(): hidden in magic_methods/set.php on line 68 in magic_methods/set.php on line 28
說明:
__set()在 $obj->a = 1;時呼叫,並改寫存到$this->data中
__get()在 echo $obj->a; 時呼叫,因為前面$obj->a = 1;被__set()改寫了,所以取不到值,會呼叫__get()
__isset() 在 isset($obj->a) 時呼叫,所以被叫了兩次
__unset() 在 unset($obj->a); 時呼叫

關於overloading
定義:藉由接收的參數串列之型態或數量之不同,以求達到多個函式可共用相同函式名稱
overloading的目的是:
1. 降低所需命名的函式名稱
2. 提高user的易用性
PHP用__call()實作method overloading( function overloading、廣義或上面定義的overloading )
__get()實作 attribute overloading( operator overloading )


__wakeup() / __sleep()
如果有在類別中定義的話,這兩個magic methods會在物件序列化( serialize() )/反序列化( unserialize() )時被呼叫。
如果類別有定義這個方法,__sleep()就會在物件開始序列化被呼叫。他會返回一個陣列,裡面列舉需要被序列化的屬性名稱。這樣在進行序列化時,系統會針對這些屬性來進行操作。

例:
class a {
    public $name;
    public function __sleep() {
        $this->name = 'fillano';
        return array("name");
    }
    public function __wakeup() {
        $this->age = 18;
    }
}
$a = new a;
$str = serialize($a);
print_r($str);
echo "\n";
$b = unserialize($str);
print_r($b);
結果:
O:1:"a":1:{s:4:"name";s:7:"fillano";}
a Object
(
    [name] => fillano
    [age] => 18
)
說明:
如果 __sleep() 沒有返回值 return array("name"); ,則會跳Notice:
PHP Notice:  serialize(): __sleep should return an array only containing the names of instance-variables to serialize in magic_methods/sleep.php on line 13

__toString()
如果定義了這個方法並且回傳一個字串,那把物件當做字串操作時,系統會呼叫__toString()來取得代表物件的字串。
例:
class hello {
    public function __toString() {
        return "Hello ";
    }
    function test() {
        echo "test\n";
    }
}
class world {
    public function __toString() {
        return "World.\n";
    }
}
$a = new hello;
$b = new world;
echo $a . $b;
$a->test();
結果:
Hello World.
test
說明:
hello->test() 依然可以呼叫,不受__toString()影響

__invoke()
這是PHP5.3才有的magic method。當物件被當做函數來呼叫時,就會呼叫這個方法。可以用is_callable()函數來檢查物件是否可以當做函數執行,可以的話會回傳true。
例:
class a {
    public function __invoke() {
     $this->name = 'Jim';
        return __CLASS__ . " is invoked.\n";
    }
}
$a = new a;
print_r($a);
if (is_callable($a)) {
    echo $a();
}
print_r($a);
結果:
a Object
(
)
a is invoked.
a Object
(
    [name] => Jim
)
說明:
在$a()觸發__invoke()前,$a->name值是空的,在__invoke()中賦值後才有

__set_state()
這個從PHP5.1加入的magic method。PHP有一個var_export()函數,可以輸出變數的結構化資訊字串,這個資訊同時也是合法的PHP程式碼,所以可以被eval()執行。
如果類別定義了這個靜態方法,當使用var_export()來處理物件實例時,系統會先檢查這個方法是否存在,然後產生呼叫這個靜態方法的程式碼字串,在程式碼中,會把物件實例的屬性陣列當做參數傳遞給他。
例:
class A
{
    public $var1;
    public $var2;

    public static function __set_state($an_array) // As of PHP 5.1.0
    {
        echo "__set_state\n";
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';

eval('$b = ' . var_export($a, true) . ';'); // $b = A::__set_state(array(
                                            //    'var1' => 5,
                                            //    'var2' => 'foo',
                                            // ));
var_dump($b);
結果:
__set_state
object(A)#2 (2) {
  ["var1"]=>
  int(5)
  ["var2"]=>
  string(3) "foo"
}
注意:
必須同時使用eval()和var_export(),如: eval('$b = ' . var_export($a, true) . ';'); 才會觸發__set_state()

__clone()
__clone()會在複製( clone )完畢時對新的物件執行,所以可以在需要時,調整複製後的物件屬性。
例:
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);
var_dump($b->obj->name);
結果:
string(7) "fillano"
string(5) "james"

物件Assignment 和 Cloning有什麼差別?
http://stackoverflow.com/questions/16893949/php-object-assignment-vs-cloning  PHP Object Assignment vs Cloning
例:
class Bar {

}

$foo = new Bar;   // $foo holds a reference to an instance of Bar
$bar = $foo;      // $bar holds a copy of the reference to the instance of Bar
$baz = &$foo;     // $baz references the same reference to the instance of Bar as $foo

$blarg = clone $foo;  // the instance of Bar that $foo referenced was copied
                      // into a new instance of Bar and $blarg now holds a reference
                      // to that new instance
$foo->bear = 'bear';
$bar->xyz = 'xyz';
$baz->ted = 'ted';

echo "\nspl_object_hash(\$foo):".spl_object_hash($foo);

echo "\$foo:";
print_r($foo);

echo "\nspl_object_hash(\$bar):".spl_object_hash($bar);
echo "\$bar:";
print_r($bar);

echo "\nspl_object_hash(\$baz):".spl_object_hash($baz);
echo "\$baz:";
print_r($baz);

echo "\nspl_object_hash(\$blarg):".spl_object_hash($blarg);
echo "\$blarg:";
print_r($blarg);
結果:
spl_object_hash($foo):000000003db193cd00000003cd585be1$foo:Bar Object
(
    [bear] => bear
    [xyz] => xyz
    [ted] => ted
)

spl_object_hash($bar):000000003db193cd00000003cd585be1$bar:Bar Object
(
    [bear] => bear
    [xyz] => xyz
    [ted] => ted
)

spl_object_hash($baz):000000003db193cd00000003cd585be1$baz:Bar Object
(
    [bear] => bear
    [xyz] => xyz
    [ted] => ted
)

spl_object_hash($blarg):000000003db193ce00000003cd585be1$blarg:Bar Object
(
)
說明:
用 assign( = )和assign reference( =& )在object上無意義,結果是一樣的,用spl_object_hash()查出物件的reference ID也一樣改變其中一個屬性,其他跟著變。但用clone時,改變$foo、$bar、$baz屬性時,$blarg不受影響。
PHP的assign( = )和assign reference( =& )在 PHP常見問題::php的refrence符号&用法 可見不同之處

assignment 和 clone的另一個差別是, clone可以觸發 __clone()魔術方法

參考資料:
http://ithelp.ithome.com.tw/question/10135522  逐步提昇PHP技術能力 - PHP的語言特性 : magic methods
http://ithelp.ithome.com.tw/question/10132318  逐步提昇PHP技術能力 - PHP的語言特性 : 多載 (overloading)
http://antrash.pixnet.net/blog/post/79139547-%E8%AB%96%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91part-8%EF%BC%9Awhy-overloading%E3%80%81overriding  Why Overloading、Overriding
http://www.5idev.com/p-php_object_clone.shtml  PHP 对象克隆 clone 关键字与 __clone() 方法
http://stackoverflow.com/questions/16893949/php-object-assignment-vs-cloning  PHP Object Assignment vs Cloning
http://stackoverflow.com/questions/1953782/php-getting-reference-id  PHP: Getting reference ID







沒有留言:

張貼留言