四、魔术方法背后的魔法

PHP 语言既可以使用过程方式,也可以使用面向对象的(OO方式)编写代码。虽然过程化方法更多的是 PHP 初始版本的残余,但即使在今天,也没有什么真正阻止我们编写完全过程化的应用程序。虽然这两种方法各有优缺点,但 OO 方法是目前最主要的方法,其优点在健壮和模块化的应用程序中更为明显,这几乎不可能与过程风格一起使用。

了解 PHP OO 模型的各个特性对于理解、编写和调试现代应用程序至关重要。神奇的方法是 PHP 语言更有趣、更神秘的特性之一。它们是 PHP 编译器在某些事件下执行的预定义类方法,例如对象初始化、对象销毁、对象转换为字符串、对象方法访问、对象属性访问、对象序列化、对象反序列化等等。

在本章中,我们将按照以下章节列表介绍 PHP 中可用的每种神奇方法的使用:

  • 使用 _ 构造()
  • 使用 _destruct()
  • 使用 _ucall()
  • 使用 _callStatic()
  • 使用 _u 集()
  • 使用 _uget()
  • 使用 _uisset()
  • 使用 _unset()
  • 使用 _usleep()
  • 使用 _uwakeup()
  • 使用 _utostring()
  • 使用 _invoke()
  • 使用 _set_state()
  • 使用 _uclone()
  • 使用 _udebuginfo()
  • 跨流行平台的使用统计

PHP language reserves all function names starting with __ as magical.

使用 _ 构造()

__construct()magic 方法代表了一个 PHP 构造函数概念,与其他 OO 语言类似。它允许开发人员进入对象创建过程。声明了__construct()方法的类,在每个新创建的对象上调用它。这允许我们在使用对象之前处理对象可能需要的任何初始化。

下面的代码片段显示了__construct()方法的最简单用法:

<?php

class User
{
  public function __construct()
  {
    var_dump('__construct');
  }
}

new User;
new User();

两个User实例将向屏幕产生相同的string(11) "__construct"输出。更复杂的示例可能包括构造函数参数。考虑下面的代码片段:

<?php

class User
{
  protected $name;
  protected $age;

  public function __construct($name, $age)
  {
    $this->name = $name;
    $this->age = $age;
    var_dump($this->name);
    var_dump($this->age);
  }
}

new User; #1
new User('John'); #2
new User('John', 34); #3
new User('John', 34, 4200.00); #4

这里,我们看到一个__construct()方法,它接受两个参数--$name$age。在User类定义之后,我们有四种不同的对象初始化尝试。尝试#3是唯一有效的初始化尝试。尝试#1#2会触发以下错误:

Warning: Missing argument 1 for User::__construct() // #1
Warning: Missing argument 2 for User::__construct() // #1 & #2

尝试#4即使无效,也不会触发错误。与其他方法不同,当__construct()被额外参数覆盖时,PHP 不会生成错误消息。

__construct()方法的另一个有趣的例子是父类。让我们考虑下面的例子:

<?php

class User
{
  protected $name;
  protected $age;

  public function __construct($name, $age)
  {
    $this->name = $name;
    $this->age = $age;
  }
}

class Employee extends User
{
  public function __construct($employeeName, $employeeAge)
  {
    var_dump($this->name);
    var_dump($this->age);
  }
}

new Employee('John', 34);

上述代码的输出如下所示:

NULL NULL

原因是如果子类定义了构造函数,则不会隐式调用父构造函数。要触发父构造函数,需要在子构造函数中运行parent::__construct()。让我们修改Employee类以实现这一点:

class Employee extends User
{
 public function __construct($employeeName, $employeeAge)
 {
   parent::__construct($employeeName, $employeeAge);
   var_dump($this->name);
   var_dump($this->age);
 }
}

这将是现在的输出:

 string(4) "John" int(34)

让我们来看看下面的例子:

<?php

class User
{
  public function __construct()
  {
    var_dump('__construct');
  }

  public static function hello($name)
  {
    return 'Hello ' . $name;
  }
}

echo User::hello('John');

这里,我们有一个简单的User类,它有一个神奇的__construct()和一个静态的hello()方法。在类定义之后,我们有一个对静态hello()方法的调用。这不会触发__construct()方法。

上述示例的唯一输出如下所示:

Hello John

__construct()方法仅在通过new关键字启动对象时触发。

我们希望将我们的__construct()方法以及其他魔术方法仅保留在public访问修饰符下。但是,如果情况需要,我们可以在混合中随意添加finally访问修饰符

考虑下面的例子:

<?php

class User
{
 public final function __construct($name)
 {
 var_dump($name);
 }
}

class Director extends User
{

}

class Employee extends User
{
 public function __construct($name)
 {
 var_dump($name);
 }
}

new User('John'); #1
new Director('John'); #2
new Employee('John'); #3

s、 初始化尝试#1#2即使使用final访问修饰符也会运行。这是因为#1实例化了定义最终__construct()方法的原始User类,而#2实例化了不尝试实现自己的__construct()方法的空Director类。初始化尝试#3将失败,导致以下错误:

Fatal error: Cannot override final method User::__construct()

这实际上是访问修饰符和覆盖的基础,而不是特定于__construct()魔术方法本身。然而,值得一提的是,可以在构造函数中使用final修饰符,因为它可能会派上用场。

除了实例化简单的对象外,OOP 中的__construct()方法的实际用途是依赖注入的形式。如今,人们普遍认为注入依赖项是处理依赖项的一种方法。虽然依赖项可以通过各种 setter 方法注入到对象中,但在一些领先的 PHP 平台(如 Magento)中,__construct()方法的使用占主导地位。

下面的代码块演示了 Magento 的vendor/magento/module-gift-message/Model/Save.php文件的__construct()方法:

 public function __construct(
   \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
   \Magento\GiftMessage\Model\MessageFactory $messageFactory,
   \Magento\Backend\Model\Session\Quote $session,
   \Magento\GiftMessage\Helper\Message $giftMessageMessage
 ) {
   $this->productRepository = $productRepository;
   $this->_messageFactory = $messageFactory;
   $this->_session = $session;
   $this->_giftMessageMessage = $giftMessageMessage;
 }

这里有几个依赖项是通过__construct()方法传递的,这似乎比前面的示例提高了很多。即便如此,大多数 Magento 的__construct()方法比这更稳健,将数十个参数传递给对象。

我们可以很容易地将__construct()方法的作用概括为类签名,它表示使用者应该如何完全实例化特定对象。

使用 _destruct()

除了构造函数之外,析构函数也是 OO 语言的一个常见特性。__destruct()魔术方法代表了这个概念。只要没有对特定对象的其他引用,就会触发该方法。当 PHP 决定显式释放对象时,或者当我们使用unset()语言构造强制它时,都可能发生这种情况。

与构造函数一样,PHP 不会隐式调用父析构函数。为了运行父析构函数,我们需要显式调用parent::__destruct()。此外,如果子类本身没有实现父类的析构函数,则子类将继承父类的析构函数。

假设我们有一个简单的User类:

<?php

class User
{
   public function __destruct()
   {
      echo '__destruct';
   }
}

User类就绪后,让我们继续查看实例创建示例:

echo 'A';
new User();
echo 'B';

// outputs "A__destructB"

这里的new User();表达式将User类的一个实例实例化为稀薄的,因为它没有将新实例化的对象赋给变量。这是 PHP 在同一行显式调用__destruct()方法的触发器,结果是A__destructB字符串:

echo 'A';
$user = new User();
echo 'B';

// outputs "AB__destruct"

这里的new User();表达式将User类的一个实例实例化为$user变量。这可以防止 PHP 立即触发,因为脚本可能会使用路径后面的$user变量。尽管如此,PHP 在断定没有引用$user变量时还是显式调用__destruct()方法,从而生成AB__destruct字符串

echo 'A';
$user = new User();
echo 'B';
unset($user);
echo 'C';

// outputs "AB__destructC"

在这里,我们稍微扩展了前面的示例。我们正在使用unset()语言构造来强制销毁表达式之间的$user变量。对unset()的调用基本上是 PHP 执行对象的__destruct()方法的隐式触发器,从而生成AB__destructC字符串

echo 'A';
$user = new User();
echo 'B';
exit;
echo 'C';

// outputs "AB__destruct"

在这里,我们在C字符串输出之前调用exit()语言构造。这是 PHP 的一个隐式触发器,不再引用$user变量,因此可以执行对象的__destruct()方法。结果输出为AB__destruct字符串。

某些情况可能会诱使我们从__destruct()方法本身内部调用exit()构造函数,因为在__destruct()中调用exit()会阻止其余的关闭例程执行。同样,从__destruct()方法引发异常将触发致命错误,但只有在脚本终止时才会引发。这绝不是处理应用程序状态的方法。

大多数时候,析构函数不是我们想要或需要自己实现的东西。很可能我们的大多数类都不需要它,因为 PHP 本身在清理方面做得很好。但是,在有些情况下,我们可能希望在对象不再被引用后立即释放该对象所消耗的资源。__destruct()方法允许在对象终止期间进行某些后续操作。

使用 _ucall()

重载是 OOP 中的一个常见术语。然而,并不是所有编程语言都以相同的方式解释它。PHP 重载的概念与其他 OO 语言截然不同。传统上,重载提供了具有相同名称但不同参数的多个方法的能力,而在 PHP 中,重载意味着动态创建方法和属性。

unfortunate misuse of the term overloading adds a layer of confusion for some developers, as the more proper term for this type of functionality might have been interpreter hooks.

PHP 中有两种支持方法重载的神奇方法:__call()__callStatic()。在本节中,我们将更仔细地了解__call()方法。

对象上下文中调用不可访问的方法时触发__call()魔术方法。此方法接受两个参数,如下所示:

public mixed __call(string $name, array $arguments)

但是__call()方法参数具有以下含义:

  • $name:这是被调用方法的名称
  • $arguments:这是一个枚举数组,包含传递给$name方法的参数

以下示例演示了在对象上下文中使用__call()方法:

<?php

class User
{
 public function __call($name, $arguments)
 {
 echo $name . ': ' . implode(', ', $arguments) . PHP_EOL;
 }

 public function bonus($amount)
 {
 echo 'bonus: ' . $amount . PHP_EOL;
 }
}

$user = new User();
$user->hello('John', 34);
$user->bonus(560.00);
$user->salary(4200.00);

User类本身只声明了__call()bonus()方法。$user对象尝试调用hello()bonus()salary()方法。这实际上意味着对象正在尝试调用两个缺少的方法:hello()salary()__call()方法对缺少的两个方法起作用,从而产生以下输出:

__call => hello: John, 34
bonus: 560
__call => salary: 4200

我们可以在 Magento 平台中找到__call()方法的一个很好的用例示例,根据vendor/magento/framework/DataObject.php类文件中的以下条目:

public function __call($method, $args)
{
   switch (substr($method, 0, 3)) {
     case 'get':
       $key = $this->_underscore(substr($method, 3));
       $index = isset($args[0]) ? $args[0] : null;
     return $this->getData($key, $index);
     case 'set':
       $key = $this->_underscore(substr($method, 3));
       $value = isset($args[0]) ? $args[0] : null;
     return $this->setData($key, $value);
     case 'uns':
       $key = $this->_underscore(substr($method, 3));
     return $this->unsetData($key);
     case 'has':
       $key = $this->_underscore(substr($method, 3));
     return isset($this->_data[$key]);
   }
   // ...
}

在不深入磁电机本身的细节的情况下,可以说他们的DataObject类在整个框架中充当根数据对象。__call()方法中的代码使其能够神奇地获取设置取消设置检查对象实例上的属性是否存在。这将在后面的表达式中使用,例如从vendor/magento/module-checkout/Controller/Cart/Configure.php文件中获取的以下条目:

$params = new \Magento\Framework\DataObject();
$params->setCategoryId(false);
$params->setConfigureMode(true);
$params->setBuyRequest($quoteItem->getBuyRequest());

好处是我们可以很容易地为DataObject的实例赋予可能存在也可能不存在的神奇方法。例如,setCategoryId()DataObject类上不存在的方法。因为它不存在,所以调用它会触发__call()方法。这一点可能不是显而易见的,所以让我们考虑另一个虚构的例子,我们的自定义类从

<?php

class User extends \Magento\Framework\DataObject
{

}

$user = new User();

$user->setName('John');
$user->setAge(34);
$user->setSalary(4200.00);

echo $user->getName();
echo $user->getAge();
echo $user->getSalary();

注意settersgetters之美和简单,我们在这里借助__call()魔术方法实现。尽管我们的User类基本上是空的,但我们继承了父__call()实现背后的魔法。

__call()方法为我们提供了一些真正有趣的可能性,其中大多数都可以作为框架或库的一部分。

使用 _callStatic()

__callStatic()魔法与__call()方法几乎相同。当__call()方法绑定到对象上下文时,__callStatic()方法绑定到静态上下文,这意味着通过范围解析操作符(::调用不可访问的方法时会触发此方法。

该方法根据以下概要接受两个参数:

public static mixed __callStatic (string $name, array $arguments)

请注意,在方法声明中使用了静态访问修饰符,该修饰符是此方法操作的静态上下文所必需的。以下示例演示了在静态上下文中使用__callStatic()方法:

<?php

class User
{
  public static function __callStatic($name, $arguments)
  {
    echo '__callStatic => ' . $name . ': ' . implode(', ', $arguments)
      . PHP_EOL;
  }

  public static function bonus($amount)
  {
  echo 'bonus: ' . $amount . PHP_EOL;
  }
}

代码将产生以下输出:

User::hello('John', 34);
User::bonus(560.00);
User::salary(4200.00);

User类本身只声明了__callStatic()bonus()方法。User类尝试调用静态hello()bonus()salary()方法。这实际上意味着该类正在尝试调用两个缺少的方法:hello()salary()__callStatic()方法对缺少的两个方法起作用,从而产生以下输出:

__callStatic => hello: John, 34
bonus: 560
__callStatic => salary: 4200

在面向对象编程中,静态上下文的使用频率低于对象上下文,这使得__callStatic()方法的使用频率低于__call()方法。

使用 _u 集()

除了方法重载之外,属性重载是 PHP 重载功能的另一个方面。PHP 中有四种支持属性重载的神奇方法:__set()__get()__isset()__unset()。在本节中,我们将仔细了解__set()方法。

当试图将数据写入无法访问的属性时,会触发__set()魔术方法

该方法接受两个参数,如下所示:

public void __set(string $name, mixed $value)

鉴于,__set()方法参数具有以下含义:

  • $name:这是与之交互的属性的名称
  • $value:这是$name属性应该设置的值

让我们来看看下面的对象上下文示例:

<?php

class User
{
  private $data = array();

  private $name;
  protected $age;
  public $salary;

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

$user = new User();
$user->name = 'John';
$user->age = 34;
$user->salary = 4200.00;
$user->message = 'hello';

var_dump($user);

User类使用各种访问修饰符声明四个属性。它进一步声明了拦截对象上下文上所有属性写入尝试的__set()方法。试图设置不存在($message或不可访问($name$age属性会触发__set()方法。__set()方法的内部工作将无法访问的数据推送到$data属性数组中,该属性数组在以下输出中可见:

object(User)#1 (4) {
  ["data":"User":private]=> array(3) {
    ["name"]=> string(4) "John"
    ["age"]=> int(34)
    ["message"]=> string(5) "hello"
  }
  ["name":"User":private]=> NULL
  ["age":protected]=> NULL
  ["salary"]=> float(4200)
}

__set()方法的一个实际用途可能是允许在对象构造期间将属性设置为true;否则,抛出一个异常。

在静态上下文中尝试使用四种属性重载方法(__set()__get()__isset()__unset()中的任何一种)将导致以下错误:

PHP Warning: The magic method __set() must have public visibility and cannot be static...

使用 _uget()

当尝试从无法访问的属性读取数据时,会触发__get()魔术方法。该方法接受单个参数,如下所示:

public mixed __get(string $name)

$name参数是与之交互的属性的名称。

让我们来看看下面的对象上下文示例:

<?php

class User
{
  private $data = [
    'name' => 'Marry',
    'age' => 32,
    'salary' => 5300.00,
  ];

  private $name = 'John';
  protected $age = 34;
  public $salary = 4200.00;

  public function __get($name)
  {
    if (array_key_exists($name, $this->data)) {
      echo '__get => ' . $name . ': ' . $this->data[$name] . PHP_EOL;
    } else {
      trigger_error('Undefined property: ' . $name, E_USER_NOTICE);
    }
  }
}

$user = new User();

echo $user->name . PHP_EOL;
echo $user->age . PHP_EOL;
echo $user->salary . PHP_EOL;
echo $user->message . PHP_EOL;

User类跨三个不同的可见性访问修饰符定义了四个不同的属性。因为我们没有访问所有单个属性的 getter 方法,所以唯一可以直接访问的属性是public $salary。这就是__get()方法派上用场的地方,因为当我们试图访问一个不存在或无法访问的属性时,__get()方法就会起作用。前面代码的结果输出归结为以下四行:

__get => name: Marry

__get => age: 32

4200

PHP Notice: Undefined property: message in...

由于__get()方法的内部工作,agename值是从$data属性中获取的。

使用 _uisset()

通过调用不可访问属性上的isset()empty()语言构造触发__isset()魔术方法。该方法接受单个参数,如下所示:

public bool __isset(string $name)

$name参数是与之交互的属性的名称。

让我们来看看下面的对象上下文示例:

<?php

class User
{
  private $data = [
    'name' => 'John',
    'age' => 34,
  ];

  public function __isset($name)
  {
    if (array_key_exists($name, $this->data)) {
      return true;
    }

    return false;
  }
}

$user = new User();

var_dump(isset($user->name));

User类定义了一个名为$data的受保护数组属性和一个神奇的__isset()方法。当前方法的内部工作只是对$data数组键名进行名称查找,如果在数组中找到键,则返回true,否则返回false。示例的结果输出为bool(true)

Magento 平台为__isset()方法提供了一个有趣而实用的用例,作为其vendor/magento/framework/HTTP/PhpEnvironment/Request.php类文件的一部分:

public function __isset($key)
{
  switch (true) {
    case isset($this->params[$key]):
    return true;

    case isset($this->queryParams[$key]):
    return true;

    case isset($this->postParams[$key]):
    return true;

    case isset($_COOKIE[$key]):
    return true;

    case isset($this->serverParams[$key]):
    return true;

    case isset($this->envParams[$key]):
    return true;

    default:
    return false;
  }
}

这里的Magento\Framework\HTTP\PhpEnvironment\Request类表示 PHP 环境及其所有可能的请求数据。请求数据可以来自多个来源:查询字符串、$_GET$_POST等。switch案例遍历这些源数据变量中的几个($params$queryParams$postParams$serverParams$envParams$_COOKIE),以查找并确认请求参数的存在。

使用 _unset()

__unset()魔术方法是通过调用不可访问属性上的unset()语言构造触发的。该方法接受单个参数,如下所示:

public bool __unset(string $name)

$name参数是与之交互的属性的名称。

让我们来看看下面的对象上下文示例:

<?php

class User
{
  private $data = [
    'name' => 'John',
    'age' => 34,
  ];

  public function __unset($name)
  {
    unset($this->data[$name]);
  }
}

$user = new User();

var_dump($user);
unset($user->age);
unset($user->salary);
var_dump($user);

User类声明了一个私有$data数组属性,以及__unset()魔术方法。方法本身相当简单;它只要求unset()在给定的数组键处传递值。我们试图在这里取消设置$age$salary属性。$salary属性实际上不存在,既不是类属性,也不是data数组键。幸运的是,unset()不会抛出Undefined index通知类型的错误,因此我们不需要额外的array_key_exists()检查。以下结果输出显示从对象实例中删除的$age属性:

object(User)#1 (1) {
  ["data":"User":private]=> array(2) {
    ["name"]=> string(4) "John"
    ["age"]=> int(34)
  }
}

object(User)#1 (1) {
  ["data":"User":private]=> array(1) {
    ["name"]=> string(4) "John"
  }
}

我们不应该混淆使用unset()构造和(unset)铸造。这两个是不同的操作,因此(unset)施法不会触发__unset()魔法

unset($user->age); // will trigger __unset()
((unset) $user->age); // won't trigger __unset()

使用 _usleep()

对象序列化是 OOP 的另一个重要方面。PHP 提供了一个serialize()函数,允许我们序列化传递给它的值。结果是一个字符串,其中包含可以存储在 PHP 中的任何值的字节流表示形式。序列化标量数据类型和简单对象非常简单,如下例所示:

<?php

$age = 34;
$name = 'John';

$obj = new stdClass();
$obj->age = 34;
$obj->name = 'John';

var_dump(serialize($age));
var_dump(serialize($name));
var_dump(serialize($obj));

结果输出如下所示:

string(5) "i:34;"
string(11) "s:4:"John";"
string(56) "O:8:"stdClass":2:{s:3:"age";i:34;s:4:"name";s:4:"John";}"

即使是一个简单的自定义类也很容易:

<?php

class User
{
  public $name = 'John';
  private $age = 34;
  protected $salary = 4200.00;
}

$user = new User();

var_dump(serialize($user));

前面的代码产生以下输出:

string(81) "O:4:"User":3:{s:4:"name";s:4:"John";s:9:"Userage";i:34;s:9:"*salary";d:4200;}"

当类的大小很大或包含资源类型引用时,就会出现问题。__sleep()神奇的方法在某种程度上解决了这些挑战。它的预期用途是提交挂起的数据或执行相关的清理任务。当我们有不需要完全序列化的大型对象时,该函数非常有用。

如果对象的__sleep()方法存在,serialize()函数将触发该对象的__sleep()方法。实际触发在序列化过程开始之前完成。这使对象能够专门列出它希望允许序列化的字段。__sleep()方法的返回值必须是一个数组,其中包含我们要序列化的所有对象属性的名称。如果该方法不返回可序列化的属性名称数组,则将NULL序列化并发出E_NOTICE

下面的示例演示了一个简单的User类和一个简单的__sleep()方法实现:

<?php

class User
{
  public $name = 'John';
  private $age = 34;
  protected $salary = 4200.00;

  public function __sleep() 
  {
    // Cleanup & other operations???
    return ['name', 'salary'];
  }
}

$user = new User();

var_dump(serialize($user));

__sleep()方法的实现清楚地表明User类仅有的两个可序列化属性是namesalary。请注意,实际名称是如何以字符串形式提供的,没有$符号,其结果如下所示:

string(60) "O:4:"User":2:{s:4:"name";s:4:"John";s:9:"*salary";d:4200;}"

Serializing objects in order to store them in a database is a dangerous practice, and should be avoided by any means possible. Rare are the cases that require complex object serialization. Even those are likely a mark of improper application design.

使用 _uwakeup()

没有serialize()方法对应物unserialize()方法,序列化对象的主题就不完整。如果serialize()方法调用触发了对象的__sleep()魔术方法,那么预期反序列化会有类似的行为是合乎逻辑的。正确地说,对给定对象调用unserialize()方法会触发其__wakeup()魔术方法。

__wakeup()的预期用途是重新建立序列化过程中可能丢失的任何资源,并执行其他重新初始化任务。

让我们来看看下面的例子:

<?php

class Backup
{
  protected $ftpClient;
  protected $ftpHost;
  protected $ftpUser;
  protected $ftpPass;

  public function __construct($host, $username, $password)
  {
    $this->ftpHost = $host;
    $this->ftpUser = $username;
    $this->ftpPass = $password;

    echo 'TEST!!!' . PHP_EOL;

    $this->connect();
  }

  public function connect()
  {
    $this->ftpClient = ftp_connect($this->ftpHost, 21, 5);
    ftp_login($this->ftpClient, $this->ftpUser, $this->ftpPass);
  }

  public function __sleep()
  {
    return ['ftpHost', 'ftpUser', 'ftpPass'];
  }

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

$backup = new Backup('test.rebex.net', 'demo', 'password');
$serialized = serialize($backup);
$unserialized = unserialize($serialized);

var_dump($backup);
var_dump($serialized);
var_dump($unserialized);

Backup类通过其构造函数接受主机、用户名和密码信息。在内部,它设置核心 PHPftp_connect()函数来建立与 FTP 服务器的连接。成功建立的连接返回我们存储在类的受保护的$ftpClient属性中的资源。由于资源不可序列化,我们确保将其从__sleep()方法返回数组中排除。这确保了序列化字符串不包含$ftpHost属性。我们在__wakeup()方法中进一步设置了$this->connect();调用,以重新初始化$ftpHost资源。整个示例产生以下输出:

object(Backup)#1 (4) {
  ["ftpClient":protected]=> resource(4) of type (FTP Buffer)
  ["ftpHost":protected]=> string(14) "test.rebex.net"
  ["ftpUser":protected]=> string(4) "demo"
  ["ftpPass":protected]=> string(8) "password"
}

string(119) "O:6:"Backup":3:{s:10:"*ftpHost";s:14:"test.rebex.net";s:10:"*ftpUser";s:4:"demo";s:10:"*ftpPass";s:8:"password";}"

object(Backup)#2 (4) {
  ["ftpClient":protected]=> resource(5) of type (FTP Buffer)
  ["ftpHost":protected]=> string(14) "test.rebex.net"
  ["ftpUser":protected]=> string(4) "demo"
  ["ftpPass":protected]=> string(8) "password"
}

__wakeup()方法排序在unserialize()函数调用期间扮演构造函数的角色。因为在反序列化过程中没有调用对象的__construct()方法,所以我们需要小心地实现必要的__wakeup()方法逻辑,以便对象可以重构它可能需要的任何资源。

使用 _utostring()

当我们在字符串上下文中使用对象时,__toString()魔术方法会触发。它允许我们决定当对象被当作字符串处理时,它将如何反应。

让我们来看看下面的例子:

<?php

class User
{
  protected $name;
  protected $age;

  public function __construct($name, $age)
  {
    $this->name = $name;
    $this->age = $age;
  }
}

$user = new User('John', 34);
echo $user;

在这里,我们有一个简单的User类,它通过构造函数方法接受$name$age参数。除此之外,没有其他东西可以指示该类应该如何响应在字符串上下文中使用它的尝试,这正是我们在类声明之后所做的,正如我们正在尝试的那样echo对象实例本身。

以其当前形式,结果输出如下:

Catchable fatal error: Object of class User could not be converted to string in...

__toString()魔术法让我们以一种简单而优雅的方式绕过这个错误:

<?php

class User
{
  protected $name;
  protected $age;

  public function __construct($name, $age)
  {
    $this->name = $name;
    $this->age = $age;
  }

  public function __toString()
  {
    return $this->name . ', age ' . $this->age;
  }
}

$user = new User('John', 34);
echo $user;

通过添加__toString()魔术方法,我们能够将对象的结果字符串表示形式裁剪为以下代码行:

John, age 34

Guzzle HTTP 客户端通过其 PSR7 HTTP 消息传递接口实现提供了一个__toString()方法的实际用例示例;然而,一些实现使用了__toString()方法。下面的代码片段是 Guzzle 实现Psr\Http\Message\StreamInterface接口的vendor/guzzlehttp/psr7/src/Stream.php类文件的部分摘录:

 public function __toString()
 {
   try {
     $this->seek(0);
     return (string) stream_get_contents($this->stream);
   } catch (\Exception $e) {
     return '';
   }
 }

对于任何逻辑丰富的__toString()实现,try...catch块几乎都是一种标准。这是因为我们不能从__toString()方法中抛出异常。因此,我们需要确保没有错误逃逸。

使用 _invoke()

当对象作为函数调用时,__invoke()魔术方法被触发。该方法接受可选数量的参数,并且能够返回各种类型的数据,或者完全不返回数据,如下所示:

mixed __invoke([ $... ])

如果一个对象类实现了__invoke()方法,我们可以通过在对象名称后面指定括号()来调用该方法。这种类型的对象称为函子或函数对象。

The Wikipedia page (https://en.wikipedia.org/wiki/Functor) provides more information on the functor.

下面的代码块说明了简单的__invoke()实现:

<?php

class User
{
  public function __invoke($name, $age)
  {
    echo $name . ', ' . $age;
  }
}

__invoke()方法可以使用对象实例作为函数触发,也可以调用call_user_func()触发,

$user = new User();

$user('John', 34); // outputs: John, 34

call_user_func($user, 'John', 34); // outputs: John, 34

使用__invoke()方法,我们将我们的类伪装成

var_dump(is_callable($user)); // true

使用__invoke()的好处之一是可以跨语言创建标准回调类型。这比通过call_user_func()函数引用函数、对象实例方法或类静态方法时使用字符串、对象和数组的组合方便得多。

__invoke()方法有助于强大的语言添加,因为我们看到了新开发模式的机会;尽管如此,它的误用可能导致代码不清晰和混乱。

使用 _set_state()

对于由var_export()函数导出的类,__set_state()魔术方法被触发(不是真的)。该方法接受单个数组类型参数并返回一个对象,如下所示:

static object __set_state(array $properties)

var_export()函数输出或返回给定变量的可解析字符串表示形式。它有点类似于var_dump()函数,只是返回的表示形式是有效的 PHP

<?php

class User
{
  public $name = 'John';
  public $age = 34;
  private $salary = 4200.00;
  protected $identifier = 'ABC';
}

$user = new User();
var_export($user); // outputs string "User::__set_state..."
var_export($user, true); // returns string "User::__set_state..."

这将产生以下输出:

User::__set_state(array(
 'name' => 'John',
 'age' => 34,
 'salary' => 4200.0,
 'identifier' => 'ABC',
))

string(113) "User::__set_state(array(
 'name' => 'John',
 'age' => 34,
 'salary' => 4200.0,
 'identifier' => 'ABC',
))"

使用var_export()函数实际上不会触发我们User类的__set_state()方法。它只会产生一个表示User::__set_state(array(...))表达式的字符串,我们可以记录、输出或通过eval()语言构造执行该表达式。

下面这段代码是一个更健壮的示例,演示了eval()的使用:

<?php

class User
{
  public $name = 'John';
  public $age = 34;
  private $salary = 4200.00;
  protected $identifier = 'ABC';

  public static function __set_state($properties)
  {
    $user = new User();

    $user->name = $properties['name'];
    $user->age = $properties['age'];
    $user->salary = $properties['salary'];
    $user->identifier = $properties['identifier'];

    return $user;
  }
}

$user = new User();
$user->name = 'Mariya';
$user->age = 32;

eval('$obj = ' . var_export($user, true) . ';');

var_dump($obj);

这将产生以下输出:

object(User)#2 (4) {
  ["name"]=> string(6) "Mariya"
  ["age"]=> int(32)
  ["salary":"User":private]=> float(4200)
  ["identifier":protected]=> string(3) "ABC"
}

知道eval()语言构造是如何非常危险的,因为它允许执行任意 PHP 代码,所以不鼓励使用它。因此,除了调试目的外,__set_state()本身的使用变得有问题。

使用 _uclone()

在新克隆的对象上触发__clone()魔术方法,其中使用clone关键字进行克隆。根据以下概要,该方法不接受任何参数,也不返回任何值:

void __clone(void)

说到对象克隆,我们倾向于区分深度复制和浅层复制。深度复制复制所有内容--对象可能指向的所有对象。浅复制尽可能少地复制,尽可能将对象引用保留为引用。虽然浅层复制可以方便地防止循环引用,但复制所有属性(无论它们是引用还是值)并不总是理想的行为。

下面的示例演示了__clone()方法的实现和clone关键字的使用:

<?php

class User
{
  public $identifier;

  public function __clone()
  {
    $this->identifier = null;
  }
}

$user = new User();
$user->identifier = 'john';

$user2 = clone $user;

var_dump($user);
var_dump($user2);

这将产生以下输出:

object(User)#1 (1) {
  ["identifier"]=> string(4) "john"
}

object(User)#2 (1) {
  ["identifier"]=> NULL
}

当提到__clone()方法时,重要的一点是它不是对克隆过程的覆盖。正常的克隆过程总是发生的,__clone()方法仅仅承担着纠正错误行为的责任,而我们通常不会对结果感到满意。

使用 _udebuginfo()

调用var_dump()函数时触发__debugInfo()魔术方法。默认情况下,var_dump()函数显示对象的所有公共、受保护和私有属性。然而,如果一个对象类实现了__debugInfo()魔术方法,我们就可以控制var_dump()函数的输出。该方法不接受任何参数,并返回要显示的键值数组,如下所示:

array __debugInfo(void)

下面的示例演示了__debugInfo()方法的实现:

<?php

class User
{
  public $name = 'John';
  public $age = 34;
  private $salary = 4200.00;
  private $bonus = 680.00;
  protected $identifier = 'ABC';
  protected $logins = 67;

  public function __debugInfo()
  {
    return [
      'name' => $this->name,
      'income' => $this->salary + $this->bonus
    ];
  }
}

$user = new User();

var_dump($user);

这将产生以下输出:

object(User)#1 (2) {
  ["name"]=> string(4) "John"
  ["income"]=> float(4880)
}

虽然__debugInfo()方法对于裁剪我们自己的var_dump()输出非常有用,但这可能不是我们在日常开发中必须做的事情。

跨流行平台的使用统计

至少可以说,PHP 生态系统是巨大的。有几十种免费的开源 CMS、CRM、购物车、博客和其他平台和库。WordPress、Drupal 和 Magento 可能是博客、内容管理、,和购物车解决方案。可从各自的网站下载:

考虑到这些流行的平台,下表从一些角度介绍了 magic 方法的使用:

| 魔术法 | WordPress 4.7(702.php 文件) | Drupal 8.2.4(8199.php 文件) | Magento CE 2.1.3(29649.php 文件) | | __construct() | 343 | 2547 | 12218 | | __destruct() | 19 | 19 | 77 | | __call() | 10 | 35 | 152 | | __callStatic() | 1. | 2. | 4. | | __get() | 23 | 31 | 125 | | __set() | 15 | 24 | 86 | | __isset() | 21 | 15 | 57 | | __unset() | 11 | 13 | 34 | | __sleep() | 0 | 46 | 103 | | __wakeup() | 0 | 10 | 94 | | __toString() | 15 | 181 | 460 | | __invoke() | 0 | 27 | 112 | | __set_state() | 0 | 3. | 5. | | __clone() | 0 | 32 | 68 | | __debugInfo() | 0 | 0 | 2. |

该表是对单个平台的整个代码库进行粗略搜索的结果。除此之外,很难得出任何结论,因为平台在.php文件的数量上存在显著差异。有一点我们可以肯定——并不是所有的魔术方法都同样流行。例如,WordPress 甚至似乎没有使用在 OOP 中很重要的__sleep()__wakeup()__invoke()方法。这可能是因为 WordPress 处理的 OO 组件不如 Magento 那么多,例如,Magento 在架构意义上更像是一个 OOP 平台。Drupal 排序位于中间,根据文件总数和使用的魔术方法。不确定或不确定,前面的表概述了 PHP 所提供的几乎所有魔术方法的有效使用。

总结

在本章中,我们详细介绍了 PHP 提供的每一种神奇方法。它们的易用性与它们给语言带来的力量一样令人印象深刻。只要恰当地命名我们的类方法,我们就能够挖掘对象状态和行为的几乎每个方面。虽然这些神奇的方法中的大多数都不是我们日常使用的方法,但它们的存在赋予了我们一些漂亮的体系结构风格和解决方案,而这在其他语言中是不容易实现的。

接下来,我们将进入 CLI 领域,以及更难以理解的 PHP 使用。