PHP中类和对象的魔术方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法, 包括: __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(),__serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone(), __debugInfo() 。

__autoload 方法是用于类的自动加载, 不属于类的魔术方法。

所有的魔术方法, 都是public的

从PHP7.4.0开始, 用__serialize, __unserialize 取代了 __sleep(), __wakeup(), 如果存在前者则不会加载后者.

  • __construct() 构造函数
  • __destruct() 析构函数
  • __call() 在对象中调用一个不可访问方法时调用
  • __callStatic() 在静态上下文中调用一个不可访问的方法时调用
  • __get() 读取不可访问属性的值时调用
  • __set() 在给不可访问属性赋值时调用
  • __isset() 当对不可访问属性调用 isset() 或 empty() 时调用
  • __unset() 当对不可访问属性调用 unset() 时调用
  • __sleep() / __serialize 执行serialize() 时会先检查是否存在该方法并先调用
  • __wakeup() / __unserialize() 执行unserialize()时会先检查是否存在该方法并先调用
  • __toString() 用于一个类被当成字符串时的回应
  • __invoke() 当尝试以调用函数的方式调用一个对象时调用
  • __set_state() 当调用 var_export() 导出类时,此静态方法会被调用
  • __clone() 当用clone关键字复制完成时调用
  • __debugInfo() 当使用var_dump打印对象的属性时调用。 如果未定义则会打印所有的public, protected 和 private 属性。

构造/析构函数 __construct, __desctruct

如果扩展类中没有定义, 则会自动继承;

如果重写了, 就会覆盖父类的定义, 如果此时需要保留父类的方法, 则需要使用 parent::__construct 和 parent::__destruct 调用.

Class Person {
    public $name;
    function __construct($name='aben') {
        $this->name = $name;
    }
}
class Student extends Person{
    public $gender;
    function __construct($name='aben', $gender='male') {
        $this->gender=$gender;
        parent::__construct($name); //如果这里不调用, name的值就无法初始化
    }
}

注意: 析构函数没有参数

方法和属性的重载

php的类使用__call 和 __callStatic进行方法的重载, 使用 __set, __get, __isset, __unset 进行类的重载

__call 和 __callStatic 重载方法

在对象中调用一个不可访问方法时会调用__call方法, 在静态上下文中调用一个不可访问的方法时调用__callStatic方法.

类中的不可访问的方法, 包括未定义的方法以及private和protected的方法.

Class Person {
    public function __call($name, $arguments){
        // 注意: $name 的值区分大小写
        echo "Calling object method '$name' " . implode(', ', $arguments). PHP_EOL;
    }
    public static function __callStatic($name, $arguments){
        // 注意: $name 的值区分大小写
        echo "Calling static method '$name' " . implode(', ', $arguments). PHP_EOL;
    }
}
$Person = new Person();
$a = $Person->setName('aben', []); // 输出: Calling object method 'setName' aben
$Person::setAge('2012-12-8'); // 输出: Calling static method 'setAge' 2012-12-8

注意: __callStatic也是静态方法

__set, __get, __isset, __unset 重载属性

public __set( string $name, mixed $value) : void
public __get( string $name) : mixed
public __isset( string $name) : bool
public __unset( string $name) : void

在给不可访问属性赋值时,__set($name,$value) 会被调用。

读取不可访问属性的值时,__get($name) 会被调用。

当对不可访问属性调用 isset() 或 empty() 时,__isset($name) 会被调用。

当对不可访问属性调用 unset() 时,__unset($name) 会被调用。

在(反)序列化过程中的__sleep和__wakeup, __serialize()和__unserialize()

public function __sleep( void) : array   //返回需要序列化的变量名称
public function __wakeup( void) : void
public __serialize ( void ) : array   //返回需要序列化的变量名称和值的键值对
public __unserialize ( array $data ) : void

__sleep() 可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。适合用于一些很大的对象但不需要全部保存的场景。

__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。

从7.4.0版本开始,用__serialize和__unserialize取代__sleep和__wakeup,但是因为__sleep于__serialize返回的数据格式不同,导致了在__wakeup和__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();
    }
    //从PHP7.4.0版本开始支持__serialize和__unserialize
    public function __serialize(): array {
        //返回的是变量名称和值得键值对
        return [
          'dsn' => $this->dsn,
          'user' => $this->username,
          'pass' => $this->password,
        ];
    }
    public function __unserialize(array $data): void {
        $this->dsn = $data['dsn'];
        $this->username = $data['user'];
        $this->password = $data['pass'];

        $this->connect();
    }
}
?>

__toString 用于一个类被当成字符串时应怎样回应

public function __toString ( void ) : string

如果一个类没有定义__toString方法, 则当其被当做字符串使用时, 会发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

注意: 不能在 __toString() 方法中抛出异常!

Class Person {
    public function __toString() {
        return 'This is class Person';
    }
}
$Person = new Person();
echo $Person;

__invoke 当尝试以调用函数的方式调用一个对象时调用

public function__invoke ([ $para,... ] ) : mixed
Class Person {
    public function __invoke($age) {
        return 'Your age is:'.$age;
    }
}
$Person = new Person();
echo $Person(18).PHP_EOL; //返回: Your age is:18
var_dump(is_callable($Person)); //返回: bool(true)

上面的代码中如果没有定义__invoke, 则 is_callable为false.

__set_state() 当调用 var_export() 导出类时,此静态方法会被调用

public static function __set_state ( array $properties ) : object
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';
eval('$b = ' . var_export($a, true) . ';'); // $b = A::__set_state(array(
                                            //    'var1' => 5,
                                            //    'var2' => 'foo',
                                            // ));
var_dump($b);

__debugInfo 当使用var_dump打印对象的属性时调用

Class Person {
    public $age;
    public $gender;
    public function __construct($age, $gender) {
        $this->age = $age;
        $this->gender = $gender;
    }
}
$Person = new Person(18, 'male');
var_dump($Person);

执行结果是:

object(Person)#1 (2) {
  ["age"]=>
  int(18)
  ["gender"]=>
  string(4) "male"
}

加上__debugInfo后就可以控制属性的输出:

Class Person {
    public $age;
    public $gender;
    public function __construct($age, $gender) {
        $this->age = $age;
        $this->gender = $gender;
    }
    public function __debugInfo() {
	    //这里控制只输出age
        return [
            'age'=>$this->age
        ];
    }
}
$Person = new Person(18, 'male');
var_dump($Person);

执行结果是:

object(Person)#1 (1) {
  ["age"]=>
  int(18)
}

__clone 当用clone关键字复制完成时调用

对象复制可以通过 clone 关键字来完成(如果可能,这将调用对象的 __clone() 方法)。对象中的 __clone() 方法不能被直接调用。

$copy_of_object = clone $object;

当对象被复制后,PHP 会对对象的所有属性执行一个浅复制(shallow copy)。 所有的引用属性仍然会是一个指向原来的变量的引用

class Account{
    public $balance;//账户余额
    public function __construct($balance) {
        $this->balance = $balance;
    }
}
class Person{
    public $id;
    public $name;//姓名
    public $age;//年龄
    public $used_names;//曾用名
    public $account;//账户.  注意: 这里是一个对象!
    public function __construct(int $id, string $name, int $age, array $used_names, Account $account) {
        $this->id=$id;
        $this->name=$name;
        $this->age=$age;
        $this->used_names = $used_names;
        $this->account = $account;
    }
    public function setAge(int $age){
        $this->age=$age;
    }
    public function getAge(){
        return $this->age;
    }
    public function setUsedNames(array $used_names){
        $this->used_names = $used_names;
    }
    public function __clone() {
        $this->id=0;
    }
}

上面定义了2个类: Person和Account,并且在Person类的clone操作时把属性id设置为0。

$Person1 = new Person(1,'aben',18, ['张三','李四'], new Account(100));
$Person2 = clone $Person1;
$Person2->id=2;
$Person2->age = 20;
$Person2->name = 'sky';
$Person2->used_names = ['王五','老六'];
$Person2->account = new Account(200);
echo serialize($Person1).PHP_EOL;
echo serialize($Person2);

执行结果是:

O:6:"Person":5:{s:2:"id";i:1;s:4:"name";s:4:"aben";s:3:"age";i:18;s:10:"used_names";a:2:{i:0;s:6:"张三";i:1;s:6:"李四";}s:7:"account";O:7:"Account":1:s:7:"balance";i:100;}}
O:6:"Person":5:{s:2:"id";i:2;s:4:"name";s:3:"sky";s:3:"age";i:20;s:10:"used_names";a:2:{i:0;s:6:"王五";i:1;s:6:"老六";}s:7:"account";O:7:"Account":1:{s:7:"balance";i:200;}}

看下结果中的Account.balance是没有问题的。 但是如果把上面的定义account的代码调整一下:

$Person2->account->balance = 200;

也就是:

$Person1 = new Person(1,'aben',18, ['张三','李四'], new Account(100));
$Person2 = clone $Person1;
$Person2->id=2;
$Person2->age = 20;
$Person2->name = 'sky';
$Person2->used_names = ['王五','老六'];
//$Person2->account = new Account(200);
$Person2->account->balance = 200; //注意这里是直接对对象属性赋值
echo serialize($Person1).PHP_EOL;
echo serialize($Person2);

执行的结果是:

O:6:"Person":5:{s:2:"id";i:1;s:4:"name";s:4:"aben";s:3:"age";i:18;s:10:"used_names";a:2:{i:0;s:6:"张三";i:1;s:6:"李四";}s:7:"account";O:7:"Account":1:s:7:"balance";i:200;}}
O:6:"Person":5:{s:2:"id";i:2;s:4:"name";s:3:"sky";s:3:"age";i:20;s:10:"used_names";a:2:{i:0;s:6:"王五";i:1;s:6:"老六";}s:7:"account";O:7:"Account":1:{s:7:"balance";i:200;}}

很显然,Person1.Account.balance的值也被改变了,所以需要对需要clone的对象的引用属性追加一次复制:

class Person{
    ....
    public function __clone() {
        $this->id=0;
        $this->account = clone $this->account; //强制复制一份, 否则仍然指向同一个对象
    }
    ....
}

如果使用复制方法 $Person2 = $Person1, 则实际上仍然中存在一个对象$Person1, 对$Person2的所有操作其实都是在对$Person1进行操作. 而clone默认只能复制普通属性.

更多信息请参考官方手册: https://www.php.net/manual/zh/language.oop5.magic.php

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值