janklan / deepcopy
创建对象的深度拷贝(克隆)
Requires
- php: ^7.1 || ^8.0
Requires (Dev)
- doctrine/collections: ^1.6.8
- doctrine/common: ^2.13.3 || ^3.2.2
- phpspec/prophecy: ^1.10
- phpunit/phpunit: ^7.5.20 || ^8.5.23 || ^9.5.13
Conflicts
- doctrine/collections: <1.6.8
- doctrine/common: <2.13.3 || >=3 <3.2.2
README
DeepCopy 帮助您创建对象的深度拷贝(克隆)。它被设计用来处理关联图中的循环。
目录
如何?
使用 Composer 安装
composer require janklan/deepcopy
使用它
use DeepCopy\DeepCopy; $copier = new DeepCopy(); $myCopy = $copier->copy($myObject);
为什么使用它?
- 您如何创建对象的副本?
$myCopy = clone $myObject;
- 您如何创建对象的 深度 副本(即复制属性中引用的所有对象)?
您使用 __clone()
并自行实现行为。
- 但是,您如何处理关联图中的 循环?
现在您将陷入一个大麻烦 :(
使用简单的 clone
重写 __clone()
使用 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
(过滤器)
如果您在代理类上使用克隆,您可能需要为
- 加载数据
- 应用转换
您可以使用 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
(类型过滤器)
- 如果您想替换属性的值
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)'
- 如果您想替换整个元素
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
操作,您有两个选项
- 手动遍历您的克隆关联并手动持久化新实体
- 使用
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对于及时考虑来说太重了。
我决定分叉项目,有几个目标
- 删除旧依赖项
- 将代码提升到PHP 8.2及以上。如果您需要使用旧软件克隆复杂对象,请参阅https://github.com/myclabs/DeepCopy/。这将是在此
janklan/deepcopy
的1.x分支上所做的最后一个重大提交。 - 添加缺失的功能-即自动持久化克隆的链接Doctrine对象的能力
- 作为一个模糊的目标,调整过滤器的工作方式。它们在
TypeFilter
和(非-Type)Filter
之间的分割方式,以及一个可以被链式使用,另一个不能的事实,这让我感到非常不舒服。
感谢@mnapoli的所有工作。