janklan/deepcopy

创建对象的深度拷贝(克隆)

维护者

详细信息

github.com/janklan/deepcopy

源代码

1.12.1 2023-11-03 04:30 UTC

This package is auto-updated.

Last update: 2024-09-03 06:32:37 UTC


README

DeepCopy 帮助您创建对象的深度拷贝(克隆)。它被设计用来处理关联图中的循环。

Total Downloads Integrate

目录

  1. 如何使用
  2. 为什么使用它
    1. 使用简单的 clone
    2. 重写 __clone()
    3. 使用 DeepCopy
  3. 它的工作原理
  4. 进一步了解
    1. 匹配器
      1. 属性名
      2. 特定属性
      3. 类型
    2. 过滤器
      1. SetNullFilter
      2. KeepFilter
      3. DoctrineCollectionFilter
      4. DoctrineEmptyCollectionFilter
      5. DoctrineProxyFilter
      6. ReplaceFilter
      7. ShallowCopyFilter
  5. 边缘情况
  6. 贡献
    1. 测试

如何?

使用 Composer 安装

composer require janklan/deepcopy

使用它

use DeepCopy\DeepCopy;

$copier = new DeepCopy();
$myCopy = $copier->copy($myObject);

为什么使用它?

  • 您如何创建对象的副本?
$myCopy = clone $myObject;
  • 您如何创建对象的 深度 副本(即复制属性中引用的所有对象)?

您使用 __clone() 并自行实现行为。

  • 但是,您如何处理关联图中的 循环

现在您将陷入一个大麻烦 :(

association graph

使用简单的 clone

Using clone

重写 __clone()

Overriding __clone

使用 DeepCopy

With DeepCopy

它的工作原理

DeepCopy 递归遍历对象的全部属性并将它们克隆。为了避免克隆相同的对象两次,它保留了一个所有实例的哈希表,从而保留了对象图。

要使用它

use function DeepCopy\deep_copy;

$copy = deep_copy($var);

或者,您可以创建自己的 DeepCopy 实例来对其进行不同的配置,例如

use DeepCopy\DeepCopy;

$copier = new DeepCopy(true);

$copy = $copier->copy($var);

您可能想自己编写深度拷贝函数

namespace Acme;

use DeepCopy\DeepCopy;

function deep_copy($var)
{
    static $copier = null;
    
    if (null === $copier) {
        $copier = new DeepCopy(true);
    }
    
    return $copier->copy($var);
}

进一步了解

您可以向复制过程添加过滤器以自定义它。

添加过滤器的方法是 DeepCopy\DeepCopy::addFilter($filter, $matcher),其中 $filter 实现 DeepCopy\Filter\Filter,而 $matcher 实现 DeepCopy\Matcher\Matcher

我们提供了一些通用的过滤器和匹配器。

匹配器

  • DeepCopy\Matcher 作用于对象属性。
  • DeepCopy\TypeMatcher 作用于图中找到的任何元素,包括数组元素。

属性名

PropertyNameMatcher 将通过属性名匹配属性

use DeepCopy\Matcher\PropertyNameMatcher;

// Will apply a filter to any property of any objects named "id"
$matcher = new PropertyNameMatcher('id');

特定属性

PropertyMatcher 将匹配特定类的特定属性

use DeepCopy\Matcher\PropertyMatcher;

// Will apply a filter to the property "id" of any objects of the class "MyClass"
$matcher = new PropertyMatcher('MyClass', 'id');

类型

TypeMatcher 将通过类型匹配任何元素(类的实例或可以是 gettype() 函数参数的任何值)

use DeepCopy\TypeMatcher\TypeMatcher;

// Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection
$matcher = new TypeMatcher('Doctrine\Common\Collections\Collection');

过滤器

  • DeepCopy\Filter 对通过 DeepCopy\Matcher 匹配的对象属性应用转换
  • DeepCopy\TypeFilter 对通过 DeepCopy\TypeMatcher 匹配的任何元素应用转换

设计上,匹配过滤器将停止过滤器链(即后续的过滤器将不会应用)。使用 ChainableFilter 不会停止过滤器链。

SetNullFilter(过滤器)

假设您正在复制一个数据库记录(或 Doctrine 实体),您希望复制的副本不包含任何 ID

use DeepCopy\DeepCopy;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\PropertyNameMatcher;

$object = MyClass::load(123);
echo $object->id; // 123

$copier = new DeepCopy();
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));

$copy = $copier->copy($object);

echo $copy->id; // null

KeepFilter(过滤器)

如果您想保持一个属性不变(例如,到一个对象的关联)

use DeepCopy\DeepCopy;
use DeepCopy\Filter\KeepFilter;
use DeepCopy\Matcher\PropertyMatcher;

$copier = new DeepCopy();
$copier->addFilter(new KeepFilter(), new PropertyMatcher('MyClass', 'category'));

$copy = $copier->copy($object);
// $copy->category has not been touched

ChainableFilter(过滤器)

如果您在代理类上使用克隆,您可能需要为

  1. 加载数据
  2. 应用转换

您可以使用 ChainableFilter 作为代理加载器过滤器的装饰器,它不会停止过滤器链(即后续的过滤器可能被应用)。

use DeepCopy\DeepCopy;
use DeepCopy\Filter\ChainableFilter;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Filter\SetNullFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
use DeepCopy\Matcher\PropertyNameMatcher;

$copier = new DeepCopy();
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));

$copy = $copier->copy($object);

echo $copy->id; // null

DoctrineCollectionFilter(过滤器)

如果您使用 Doctrine 并想复制一个实体,您将需要使用 DoctrineCollectionFilter

use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
use DeepCopy\Matcher\PropertyTypeMatcher;

$copier = new DeepCopy();
$copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection'));

$copy = $copier->copy($object);

DoctrineEmptyCollectionFilter(过滤器)

如果您使用Doctrine并希望复制一个包含您想要重置的Collection的实体,您可以使用DoctrineEmptyCollectionFilter

use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
use DeepCopy\Matcher\PropertyMatcher;

$copier = new DeepCopy();
$copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher('MyClass', 'myProperty'));

$copy = $copier->copy($object);

// $copy->myProperty will return an empty collection

DoctrineProxyFilter(过滤器)

如果您使用Doctrine并在懒加载的实体上使用克隆,您可能会遇到提及在Doctrine代理类(...\__CG__\Proxy)上缺少字段的错误。您可以使用DoctrineProxyFilter来加载Doctrine代理类背后的实际实体。但是,请确保将其作为您第一个过滤器之一放入过滤器链,以便在应用其他过滤器之前加载实体!我们建议用ChainableFilter装饰DoctrineProxyFilter,以允许对克隆的懒加载实体应用其他过滤器。

use DeepCopy\DeepCopy;
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;

$copier = new DeepCopy();
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());

$copy = $copier->copy($object);

// $copy should now contain a clone of all entities, including those that were not yet fully loaded.

ReplaceFilter(类型过滤器)

  1. 如果您想替换属性的值
use DeepCopy\DeepCopy;
use DeepCopy\Filter\ReplaceFilter;
use DeepCopy\Matcher\PropertyMatcher;

$copier = new DeepCopy();
$callback = function ($currentValue) {
  return $currentValue . ' (copy)'
};
$copier->addFilter(new ReplaceFilter($callback), new PropertyMatcher('MyClass', 'title'));

$copy = $copier->copy($object);

// $copy->title will contain the data returned by the callback, e.g. 'The title (copy)'
  1. 如果您想替换整个元素
use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ReplaceFilter;
use DeepCopy\TypeMatcher\TypeMatcher;

$copier = new DeepCopy();
$callback = function (MyClass $myClass) {
  return get_class($myClass);
};
$copier->addTypeFilter(new ReplaceFilter($callback), new TypeMatcher('MyClass'));

$copy = $copier->copy([new MyClass, 'some string', new MyClass]);

// $copy will contain ['MyClass', 'some string', 'MyClass']

ReplaceFilter构造函数的$callback参数接受任何PHP可调用者。

ShallowCopyFilter(类型过滤器)

停止深拷贝递归复制元素,使用标准clone代替

use DeepCopy\DeepCopy;
use DeepCopy\TypeFilter\ShallowCopyFilter;
use DeepCopy\TypeMatcher\TypeMatcher;
use Mockery as m;

$this->deepCopy = new DeepCopy();
$this->deepCopy->addTypeFilter(
	new ShallowCopyFilter,
	new TypeMatcher(m\MockInterface::class)
);

$myServiceWithMocks = new MyService(m::mock(MyDependency1::class), m::mock(MyDependency2::class));
// All mocks will be just cloned, not deep copied

持久化克隆的Doctrine实体

如果您正在克隆Doctrine实体并且没有自动级联persist操作,您有两个选项

  1. 手动遍历您的克隆关联并手动持久化新实体
  2. 使用DeepCopy::onObjectCopied回调在克隆过程的末尾处理每个克隆对象。

以下是一个onObjectCopied回调示例,它会持久化您的实体。

$copier = new DeepCopy();

/**
 * @var EntityManagerInterface $entityManager
 * @var DeepCopy $copier 
 */
$copier->onObjectCopied = function (object $object) use ($entityManager) {
    $entityManager->persist($object);
};

边缘情况

以下结构不能使用PHP反射进行深拷贝。因此,它们被浅拷贝,并且过滤器不适用。您有两种方法来处理这些

  • 实现自己的__clone()方法
  • 使用类型匹配器过滤器

贡献

DeepCopy在MIT许可下分发。

测试

运行测试很简单

vendor/bin/phpunit

致谢

这是https://github.com/myclabs/DeepCopy/的一个分支,这是一个下载量超过百万的非常受欢迎的库,这意味着它固有的遗留问题:它需要支持旧代码。在某个阶段,我比它更需要这个库,而PR对于及时考虑来说太重了。

我决定分叉项目,有几个目标

  1. 删除旧依赖项
  2. 将代码提升到PHP 8.2及以上。如果您需要使用旧软件克隆复杂对象,请参阅https://github.com/myclabs/DeepCopy/。这将是在此janklan/deepcopy的1.x分支上所做的最后一个重大提交。
  3. 添加缺失的功能-即自动持久化克隆的链接Doctrine对象的能力
  4. 作为一个模糊的目标,调整过滤器的工作方式。它们在TypeFilter(非-Type)Filter之间的分割方式,以及一个可以被链式使用,另一个不能的事实,这让我感到非常不舒服。

感谢@mnapoli的所有工作。