jesseschalken / magic-utils

PHP的魔术方法实现辅助特性和函数

0.2 2016-01-03 04:34 UTC

This package is not auto-updated.

Last update: 2024-09-14 18:36:46 UTC


README

php-magic-utils 提供了特性和函数,帮助实现 PHP的魔术方法,特别是 __clone()

使用 DeepClone;

DeepClone

class Foo extends Bar {
    /** @var Blah|null */
    private $blah;
    /** @var \DateTime[] */
    private $dates = [];

    // :( :( :(
    function __clone() {
        parent::__clone();

        if ($this->blah !== null)
            $this->blah = clone $this->blah;

        foreach ($this->dates as $k => $date)
            $this->dates[$k] = clone $date;
    }
}

转换为

class Foo extends Bar {
    /** @var Blah|null */
    private $blah;
    /** @var \DateTime[] */
    private $dates = [];
    
    // :) :) :)
    use DeepClone;
}

它通过调用 parent::__clone()(如果存在)来实现 __clone(),并克隆使用该类的所有属性中包含的所有对象,包括任意嵌套数组中的对象。如果它找到一个 resource 类型,则会报错,因为 resource 与对象一样是按引用传递的,因此应该进行克隆,但没有通用的克隆 resource 的方法。

注意,您必须在类层次结构的每一层使用 use DeepClone;。它不会克隆父类或派生类的属性。

为什么要进行深度克隆?

当使用 clone ... 复制对象时,PHP 默认执行的是 浅克隆,这意味着创建了一个新对象,其所有属性具有相同的值,但这些属性中包含的对象实例是共享的。这可能导致类的用户受到直接存储的信息和通过其他对象间接存储的信息的影响,破坏抽象并导致微妙的错误。

以下是一个示例

class A {
    private $foo = 9;
    public function getFoo() { return $this->foo; }
    public function setFoo($foo) { $this->foo = $foo; }
}
function test() {
    $a1 = new A;
    $a1->setFoo(100);
    $a2 = clone $a1;
    $a2->setFoo(200);
    print $a1->getFoo(); // 100
}

如果对 $foo 属性进行了一次无辜的重构,将其值移动到另一个对象(B)中

class A {
    private $b;
    public function __construct() {
        $this->b = new B;
    }
    public function getFoo() { return $this->b->foo; }
    public function setFoo($foo) { $this->b->foo = $foo; }
}

class B {
    public $foo = 9;
}

现在 test() 函数将打印 200 而不是 100,因为 $a1$a2 将共享 B 的同一个实例。

可以通过实现 __clone() 来解决这个问题,通过执行深度克隆

class A {
    private $b;
    // ...
    public function __clone() {
        $this->b = clone $this->b;
    }
    // ...
}

进行重构的开发者必须记住更新 __clone() 方法以保持行为的一致性,以防对象被克隆。use DeepClone; 为您做了这件事,所以您不必记住。

clone_ref(mixed &$x):void

将克隆指定变量中包含的所有对象,包括嵌套数组中的对象。这对于通过深度克隆仅一些属性来实现 __clone() 很有用。

例如

class A {
    private $prop1;
    private $prop2;
    function __construct() {
        $this->prop1 = new Foo1;
        $this->prop2 = new Foo2;
    }
    function __clone() {
        clone_ref($this->prop2);
        // cloned instances will share the same object stored in $this->prop1
    }
}

clone_val(mixed $x):mixed

将克隆指定值中包含的所有对象,并返回新的值。

使用 NoClone;

use NoClone; 阻止对象被克隆。它通过抛出 CloneNotSupportedException 来实现 __clone()

NoDynamicMethods;

魔术方法 __call()__callStatic() 在添加新方法到类时可能不安全,因为新方法可能会意外地覆盖由 __call()__callStatic() 处理的动态方法。

使用 NoDynamicMethods; 将 __call()__callStatic() 定义为抛出 UndefinedMethodException,因此添加新方法是始终安全的。

use NoDynamicProperties;

魔术方法 __get()__set()__isset()__unset() 在添加新属性到类时可能不安全,因为新属性可能会意外地覆盖由这些魔术方法处理的动态属性。

即使没有定义这些魔术方法,类上的新属性可能已经被用作未声明的公共属性,例如

class Foo {
}
function blah(Foo $foo) {
    $foo->bar = 5;
    return $foo->bar;
}

blah()中使用未声明的属性Foo::$bar使得Foo的作者无法安全地添加新的属性Foo::$bar

使用use NoDynamicProperties;定义了__get()__set()__isset()__unset()来抛出UndefinedPropertyException异常,因此添加新属性总是安全的。

use NoSerialize;

PHP内建的serialize()unserialize()函数使得更改类的名称或属性具有潜在的不安全性,因为可能存在已序列化的类版本,在反序列化时需要继续工作。

使用use NoSerialize;定义了__sleep()__wakeup()来抛出SerializeNotSupportedException异常,因此更改类名或其属性总是安全的。

use NoMagic;

use NoMagic;禁止任何导致重构困难的魔术方法。它等价于

use NoDynamicMethods;
use NoDynamicProperties;
use NoSerialize;