├── LICENSE ├── README.md ├── composer.json └── src └── DeepCopy ├── DeepCopy.php ├── Exception ├── CloneException.php └── PropertyException.php ├── Filter ├── ChainableFilter.php ├── Doctrine │ ├── DoctrineCollectionFilter.php │ ├── DoctrineEmptyCollectionFilter.php │ └── DoctrineProxyFilter.php ├── Filter.php ├── KeepFilter.php ├── ReplaceFilter.php └── SetNullFilter.php ├── Matcher ├── Doctrine │ └── DoctrineProxyMatcher.php ├── Matcher.php ├── PropertyMatcher.php ├── PropertyNameMatcher.php └── PropertyTypeMatcher.php ├── Reflection └── ReflectionHelper.php ├── TypeFilter ├── Date │ ├── DateIntervalFilter.php │ └── DatePeriodFilter.php ├── ReplaceFilter.php ├── ShallowCopyFilter.php ├── Spl │ ├── ArrayObjectFilter.php │ ├── SplDoublyLinkedList.php │ └── SplDoublyLinkedListFilter.php └── TypeFilter.php ├── TypeMatcher └── TypeMatcher.php └── deep_copy.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 My C-Sense 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepCopy 2 | 3 | DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph. 4 | 5 | [![Total Downloads](https://poser.pugx.org/myclabs/deep-copy/downloads.svg)](https://packagist.org/packages/myclabs/deep-copy) 6 | [![Integrate](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml/badge.svg?branch=1.x)](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml) 7 | 8 | ## Table of Contents 9 | 10 | 1. [How](#how) 11 | 1. [Why](#why) 12 | 1. [Using simply `clone`](#using-simply-clone) 13 | 1. [Overriding `__clone()`](#overriding-__clone) 14 | 1. [With `DeepCopy`](#with-deepcopy) 15 | 1. [How it works](#how-it-works) 16 | 1. [Going further](#going-further) 17 | 1. [Matchers](#matchers) 18 | 1. [Property name](#property-name) 19 | 1. [Specific property](#specific-property) 20 | 1. [Type](#type) 21 | 1. [Filters](#filters) 22 | 1. [`SetNullFilter`](#setnullfilter-filter) 23 | 1. [`KeepFilter`](#keepfilter-filter) 24 | 1. [`DoctrineCollectionFilter`](#doctrinecollectionfilter-filter) 25 | 1. [`DoctrineEmptyCollectionFilter`](#doctrineemptycollectionfilter-filter) 26 | 1. [`DoctrineProxyFilter`](#doctrineproxyfilter-filter) 27 | 1. [`ReplaceFilter`](#replacefilter-type-filter) 28 | 1. [`ShallowCopyFilter`](#shallowcopyfilter-type-filter) 29 | 1. [Edge cases](#edge-cases) 30 | 1. [Contributing](#contributing) 31 | 1. [Tests](#tests) 32 | 33 | 34 | ## How? 35 | 36 | Install with Composer: 37 | 38 | ``` 39 | composer require myclabs/deep-copy 40 | ``` 41 | 42 | Use it: 43 | 44 | ```php 45 | use DeepCopy\DeepCopy; 46 | 47 | $copier = new DeepCopy(); 48 | $myCopy = $copier->copy($myObject); 49 | ``` 50 | 51 | 52 | ## Why? 53 | 54 | - How do you create copies of your objects? 55 | 56 | ```php 57 | $myCopy = clone $myObject; 58 | ``` 59 | 60 | - How do you create **deep** copies of your objects (i.e. copying also all the objects referenced in the properties)? 61 | 62 | You use [`__clone()`](http://www.php.net/manual/en/language.oop5.cloning.php#object.clone) and implement the behavior 63 | yourself. 64 | 65 | - But how do you handle **cycles** in the association graph? 66 | 67 | Now you're in for a big mess :( 68 | 69 | ![association graph](doc/graph.png) 70 | 71 | 72 | ### Using simply `clone` 73 | 74 | ![Using clone](doc/clone.png) 75 | 76 | 77 | ### Overriding `__clone()` 78 | 79 | ![Overriding __clone](doc/deep-clone.png) 80 | 81 | 82 | ### With `DeepCopy` 83 | 84 | ![With DeepCopy](doc/deep-copy.png) 85 | 86 | 87 | ## How it works 88 | 89 | DeepCopy recursively traverses all the object's properties and clones them. To avoid cloning the same object twice it 90 | keeps a hash map of all instances and thus preserves the object graph. 91 | 92 | To use it: 93 | 94 | ```php 95 | use function DeepCopy\deep_copy; 96 | 97 | $copy = deep_copy($var); 98 | ``` 99 | 100 | Alternatively, you can create your own `DeepCopy` instance to configure it differently for example: 101 | 102 | ```php 103 | use DeepCopy\DeepCopy; 104 | 105 | $copier = new DeepCopy(true); 106 | 107 | $copy = $copier->copy($var); 108 | ``` 109 | 110 | You may want to roll your own deep copy function: 111 | 112 | ```php 113 | namespace Acme; 114 | 115 | use DeepCopy\DeepCopy; 116 | 117 | function deep_copy($var) 118 | { 119 | static $copier = null; 120 | 121 | if (null === $copier) { 122 | $copier = new DeepCopy(true); 123 | } 124 | 125 | return $copier->copy($var); 126 | } 127 | ``` 128 | 129 | 130 | ## Going further 131 | 132 | You can add filters to customize the copy process. 133 | 134 | The method to add a filter is `DeepCopy\DeepCopy::addFilter($filter, $matcher)`, 135 | with `$filter` implementing `DeepCopy\Filter\Filter` 136 | and `$matcher` implementing `DeepCopy\Matcher\Matcher`. 137 | 138 | We provide some generic filters and matchers. 139 | 140 | 141 | ### Matchers 142 | 143 | - `DeepCopy\Matcher` applies on a object attribute. 144 | - `DeepCopy\TypeMatcher` applies on any element found in graph, including array elements. 145 | 146 | 147 | #### Property name 148 | 149 | The `PropertyNameMatcher` will match a property by its name: 150 | 151 | ```php 152 | use DeepCopy\Matcher\PropertyNameMatcher; 153 | 154 | // Will apply a filter to any property of any objects named "id" 155 | $matcher = new PropertyNameMatcher('id'); 156 | ``` 157 | 158 | 159 | #### Specific property 160 | 161 | The `PropertyMatcher` will match a specific property of a specific class: 162 | 163 | ```php 164 | use DeepCopy\Matcher\PropertyMatcher; 165 | 166 | // Will apply a filter to the property "id" of any objects of the class "MyClass" 167 | $matcher = new PropertyMatcher('MyClass', 'id'); 168 | ``` 169 | 170 | 171 | #### Type 172 | 173 | The `TypeMatcher` will match any element by its type (instance of a class or any value that could be parameter of 174 | [gettype()](http://php.net/manual/en/function.gettype.php) function): 175 | 176 | ```php 177 | use DeepCopy\TypeMatcher\TypeMatcher; 178 | 179 | // Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection 180 | $matcher = new TypeMatcher('Doctrine\Common\Collections\Collection'); 181 | ``` 182 | 183 | 184 | ### Filters 185 | 186 | - `DeepCopy\Filter` applies a transformation to the object attribute matched by `DeepCopy\Matcher` 187 | - `DeepCopy\TypeFilter` applies a transformation to any element matched by `DeepCopy\TypeMatcher` 188 | 189 | By design, matching a filter will stop the chain of filters (i.e. the next ones will not be applied). 190 | Using the ([`ChainableFilter`](#chainablefilter-filter)) won't stop the chain of filters. 191 | 192 | 193 | #### `SetNullFilter` (filter) 194 | 195 | Let's say for example that you are copying a database record (or a Doctrine entity), so you want the copy not to have 196 | any ID: 197 | 198 | ```php 199 | use DeepCopy\DeepCopy; 200 | use DeepCopy\Filter\SetNullFilter; 201 | use DeepCopy\Matcher\PropertyNameMatcher; 202 | 203 | $object = MyClass::load(123); 204 | echo $object->id; // 123 205 | 206 | $copier = new DeepCopy(); 207 | $copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id')); 208 | 209 | $copy = $copier->copy($object); 210 | 211 | echo $copy->id; // null 212 | ``` 213 | 214 | 215 | #### `KeepFilter` (filter) 216 | 217 | If you want a property to remain untouched (for example, an association to an object): 218 | 219 | ```php 220 | use DeepCopy\DeepCopy; 221 | use DeepCopy\Filter\KeepFilter; 222 | use DeepCopy\Matcher\PropertyMatcher; 223 | 224 | $copier = new DeepCopy(); 225 | $copier->addFilter(new KeepFilter(), new PropertyMatcher('MyClass', 'category')); 226 | 227 | $copy = $copier->copy($object); 228 | // $copy->category has not been touched 229 | ``` 230 | 231 | 232 | #### `ChainableFilter` (filter) 233 | 234 | If you use cloning on proxy classes, you might want to apply two filters for: 235 | 1. loading the data 236 | 2. applying a transformation 237 | 238 | You can use the `ChainableFilter` as a decorator of the proxy loader filter, which won't stop the chain of filters (i.e. 239 | the next ones may be applied). 240 | 241 | 242 | ```php 243 | use DeepCopy\DeepCopy; 244 | use DeepCopy\Filter\ChainableFilter; 245 | use DeepCopy\Filter\Doctrine\DoctrineProxyFilter; 246 | use DeepCopy\Filter\SetNullFilter; 247 | use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher; 248 | use DeepCopy\Matcher\PropertyNameMatcher; 249 | 250 | $copier = new DeepCopy(); 251 | $copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher()); 252 | $copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id')); 253 | 254 | $copy = $copier->copy($object); 255 | 256 | echo $copy->id; // null 257 | ``` 258 | 259 | 260 | #### `DoctrineCollectionFilter` (filter) 261 | 262 | If you use Doctrine and want to copy an entity, you will need to use the `DoctrineCollectionFilter`: 263 | 264 | ```php 265 | use DeepCopy\DeepCopy; 266 | use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter; 267 | use DeepCopy\Matcher\PropertyTypeMatcher; 268 | 269 | $copier = new DeepCopy(); 270 | $copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection')); 271 | 272 | $copy = $copier->copy($object); 273 | ``` 274 | 275 | 276 | #### `DoctrineEmptyCollectionFilter` (filter) 277 | 278 | If you use Doctrine and want to copy an entity who contains a `Collection` that you want to be reset, you can use the 279 | `DoctrineEmptyCollectionFilter` 280 | 281 | ```php 282 | use DeepCopy\DeepCopy; 283 | use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter; 284 | use DeepCopy\Matcher\PropertyMatcher; 285 | 286 | $copier = new DeepCopy(); 287 | $copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher('MyClass', 'myProperty')); 288 | 289 | $copy = $copier->copy($object); 290 | 291 | // $copy->myProperty will return an empty collection 292 | ``` 293 | 294 | 295 | #### `DoctrineProxyFilter` (filter) 296 | 297 | If you use Doctrine and use cloning on lazy loaded entities, you might encounter errors mentioning missing fields on a 298 | Doctrine proxy class (...\\\_\_CG\_\_\Proxy). 299 | You can use the `DoctrineProxyFilter` to load the actual entity behind the Doctrine proxy class. 300 | **Make sure, though, to put this as one of your very first filters in the filter chain so that the entity is loaded 301 | before other filters are applied!** 302 | We recommend to decorate the `DoctrineProxyFilter` with the `ChainableFilter` to allow applying other filters to the 303 | cloned lazy loaded entities. 304 | 305 | ```php 306 | use DeepCopy\DeepCopy; 307 | use DeepCopy\Filter\Doctrine\DoctrineProxyFilter; 308 | use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher; 309 | 310 | $copier = new DeepCopy(); 311 | $copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher()); 312 | 313 | $copy = $copier->copy($object); 314 | 315 | // $copy should now contain a clone of all entities, including those that were not yet fully loaded. 316 | ``` 317 | 318 | 319 | #### `ReplaceFilter` (type filter) 320 | 321 | 1. If you want to replace the value of a property: 322 | 323 | ```php 324 | use DeepCopy\DeepCopy; 325 | use DeepCopy\Filter\ReplaceFilter; 326 | use DeepCopy\Matcher\PropertyMatcher; 327 | 328 | $copier = new DeepCopy(); 329 | $callback = function ($currentValue) { 330 | return $currentValue . ' (copy)' 331 | }; 332 | $copier->addFilter(new ReplaceFilter($callback), new PropertyMatcher('MyClass', 'title')); 333 | 334 | $copy = $copier->copy($object); 335 | 336 | // $copy->title will contain the data returned by the callback, e.g. 'The title (copy)' 337 | ``` 338 | 339 | 2. If you want to replace whole element: 340 | 341 | ```php 342 | use DeepCopy\DeepCopy; 343 | use DeepCopy\TypeFilter\ReplaceFilter; 344 | use DeepCopy\TypeMatcher\TypeMatcher; 345 | 346 | $copier = new DeepCopy(); 347 | $callback = function (MyClass $myClass) { 348 | return get_class($myClass); 349 | }; 350 | $copier->addTypeFilter(new ReplaceFilter($callback), new TypeMatcher('MyClass')); 351 | 352 | $copy = $copier->copy([new MyClass, 'some string', new MyClass]); 353 | 354 | // $copy will contain ['MyClass', 'some string', 'MyClass'] 355 | ``` 356 | 357 | 358 | The `$callback` parameter of the `ReplaceFilter` constructor accepts any PHP callable. 359 | 360 | 361 | #### `ShallowCopyFilter` (type filter) 362 | 363 | Stop *DeepCopy* from recursively copying element, using standard `clone` instead: 364 | 365 | ```php 366 | use DeepCopy\DeepCopy; 367 | use DeepCopy\TypeFilter\ShallowCopyFilter; 368 | use DeepCopy\TypeMatcher\TypeMatcher; 369 | use Mockery as m; 370 | 371 | $this->deepCopy = new DeepCopy(); 372 | $this->deepCopy->addTypeFilter( 373 | new ShallowCopyFilter, 374 | new TypeMatcher(m\MockInterface::class) 375 | ); 376 | 377 | $myServiceWithMocks = new MyService(m::mock(MyDependency1::class), m::mock(MyDependency2::class)); 378 | // All mocks will be just cloned, not deep copied 379 | ``` 380 | 381 | 382 | ## Edge cases 383 | 384 | The following structures cannot be deep-copied with PHP Reflection. As a result they are shallow cloned and filters are 385 | not applied. There is two ways for you to handle them: 386 | 387 | - Implement your own `__clone()` method 388 | - Use a filter with a type matcher 389 | 390 | 391 | ## Contributing 392 | 393 | DeepCopy is distributed under the MIT license. 394 | 395 | 396 | ### Tests 397 | 398 | Running the tests is simple: 399 | 400 | ```php 401 | vendor/bin/phpunit 402 | ``` 403 | 404 | ### Support 405 | 406 | Get professional support via [the Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-myclabs-deep-copy?utm_source=packagist-myclabs-deep-copy&utm_medium=referral&utm_campaign=readme). 407 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myclabs/deep-copy", 3 | "description": "Create deep copies (clones) of your objects", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "clone", 8 | "copy", 9 | "duplicate", 10 | "object", 11 | "object graph" 12 | ], 13 | "require": { 14 | "php": "^7.1 || ^8.0" 15 | }, 16 | "require-dev": { 17 | "doctrine/collections": "^1.6.8", 18 | "doctrine/common": "^2.13.3 || ^3.2.2", 19 | "phpspec/prophecy": "^1.10", 20 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 21 | }, 22 | "conflict": { 23 | "doctrine/collections": "<1.6.8", 24 | "doctrine/common": "<2.13.3 || >=3 <3.2.2" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "DeepCopy\\": "src/DeepCopy/" 29 | }, 30 | "files": [ 31 | "src/DeepCopy/deep_copy.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "DeepCopyTest\\": "tests/DeepCopyTest/", 37 | "DeepCopy\\": "fixtures/" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DeepCopy/DeepCopy.php: -------------------------------------------------------------------------------- 1 | Filter, 'matcher' => Matcher] pairs. 39 | */ 40 | private $filters = []; 41 | 42 | /** 43 | * Type Filters to apply. 44 | * 45 | * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs. 46 | */ 47 | private $typeFilters = []; 48 | 49 | /** 50 | * @var bool 51 | */ 52 | private $skipUncloneable = false; 53 | 54 | /** 55 | * @var bool 56 | */ 57 | private $useCloneMethod; 58 | 59 | /** 60 | * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used 61 | * instead of the regular deep cloning. 62 | */ 63 | public function __construct($useCloneMethod = false) 64 | { 65 | $this->useCloneMethod = $useCloneMethod; 66 | 67 | $this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class)); 68 | $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class)); 69 | $this->addTypeFilter(new DatePeriodFilter(), new TypeMatcher(DatePeriod::class)); 70 | $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class)); 71 | } 72 | 73 | /** 74 | * If enabled, will not throw an exception when coming across an uncloneable property. 75 | * 76 | * @param $skipUncloneable 77 | * 78 | * @return $this 79 | */ 80 | public function skipUncloneable($skipUncloneable = true) 81 | { 82 | $this->skipUncloneable = $skipUncloneable; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Deep copies the given object. 89 | * 90 | * @param mixed $object 91 | * 92 | * @return mixed 93 | */ 94 | public function copy($object) 95 | { 96 | $this->hashMap = []; 97 | 98 | return $this->recursiveCopy($object); 99 | } 100 | 101 | public function addFilter(Filter $filter, Matcher $matcher) 102 | { 103 | $this->filters[] = [ 104 | 'matcher' => $matcher, 105 | 'filter' => $filter, 106 | ]; 107 | } 108 | 109 | public function prependFilter(Filter $filter, Matcher $matcher) 110 | { 111 | array_unshift($this->filters, [ 112 | 'matcher' => $matcher, 113 | 'filter' => $filter, 114 | ]); 115 | } 116 | 117 | public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher) 118 | { 119 | $this->typeFilters[] = [ 120 | 'matcher' => $matcher, 121 | 'filter' => $filter, 122 | ]; 123 | } 124 | 125 | public function prependTypeFilter(TypeFilter $filter, TypeMatcher $matcher) 126 | { 127 | array_unshift($this->typeFilters, [ 128 | 'matcher' => $matcher, 129 | 'filter' => $filter, 130 | ]); 131 | } 132 | 133 | private function recursiveCopy($var) 134 | { 135 | // Matches Type Filter 136 | if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) { 137 | return $filter->apply($var); 138 | } 139 | 140 | // Resource 141 | if (is_resource($var)) { 142 | return $var; 143 | } 144 | 145 | // Array 146 | if (is_array($var)) { 147 | return $this->copyArray($var); 148 | } 149 | 150 | // Scalar 151 | if (! is_object($var)) { 152 | return $var; 153 | } 154 | 155 | // Enum 156 | if (PHP_VERSION_ID >= 80100 && enum_exists(get_class($var))) { 157 | return $var; 158 | } 159 | 160 | // Object 161 | return $this->copyObject($var); 162 | } 163 | 164 | /** 165 | * Copy an array 166 | * @param array $array 167 | * @return array 168 | */ 169 | private function copyArray(array $array) 170 | { 171 | foreach ($array as $key => $value) { 172 | $array[$key] = $this->recursiveCopy($value); 173 | } 174 | 175 | return $array; 176 | } 177 | 178 | /** 179 | * Copies an object. 180 | * 181 | * @param object $object 182 | * 183 | * @throws CloneException 184 | * 185 | * @return object 186 | */ 187 | private function copyObject($object) 188 | { 189 | $objectHash = spl_object_hash($object); 190 | 191 | if (isset($this->hashMap[$objectHash])) { 192 | return $this->hashMap[$objectHash]; 193 | } 194 | 195 | $reflectedObject = new ReflectionObject($object); 196 | $isCloneable = $reflectedObject->isCloneable(); 197 | 198 | if (false === $isCloneable) { 199 | if ($this->skipUncloneable) { 200 | $this->hashMap[$objectHash] = $object; 201 | 202 | return $object; 203 | } 204 | 205 | throw new CloneException( 206 | sprintf( 207 | 'The class "%s" is not cloneable.', 208 | $reflectedObject->getName() 209 | ) 210 | ); 211 | } 212 | 213 | $newObject = clone $object; 214 | $this->hashMap[$objectHash] = $newObject; 215 | 216 | if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { 217 | return $newObject; 218 | } 219 | 220 | if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) { 221 | return $newObject; 222 | } 223 | 224 | foreach (ReflectionHelper::getProperties($reflectedObject) as $property) { 225 | $this->copyObjectProperty($newObject, $property); 226 | } 227 | 228 | return $newObject; 229 | } 230 | 231 | private function copyObjectProperty($object, ReflectionProperty $property) 232 | { 233 | // Ignore static properties 234 | if ($property->isStatic()) { 235 | return; 236 | } 237 | 238 | // Ignore readonly properties 239 | if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) { 240 | return; 241 | } 242 | 243 | // Apply the filters 244 | foreach ($this->filters as $item) { 245 | /** @var Matcher $matcher */ 246 | $matcher = $item['matcher']; 247 | /** @var Filter $filter */ 248 | $filter = $item['filter']; 249 | 250 | if ($matcher->matches($object, $property->getName())) { 251 | $filter->apply( 252 | $object, 253 | $property->getName(), 254 | function ($object) { 255 | return $this->recursiveCopy($object); 256 | } 257 | ); 258 | 259 | if ($filter instanceof ChainableFilter) { 260 | continue; 261 | } 262 | 263 | // If a filter matches, we stop processing this property 264 | return; 265 | } 266 | } 267 | 268 | $property->setAccessible(true); 269 | 270 | // Ignore uninitialized properties (for PHP >7.4) 271 | if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) { 272 | return; 273 | } 274 | 275 | $propertyValue = $property->getValue($object); 276 | 277 | // Copy the property 278 | $property->setValue($object, $this->recursiveCopy($propertyValue)); 279 | } 280 | 281 | /** 282 | * Returns first filter that matches variable, `null` if no such filter found. 283 | * 284 | * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 285 | * 'matcher' with value of type {@see TypeMatcher} 286 | * @param mixed $var 287 | * 288 | * @return TypeFilter|null 289 | */ 290 | private function getFirstMatchedTypeFilter(array $filterRecords, $var) 291 | { 292 | $matched = $this->first( 293 | $filterRecords, 294 | function (array $record) use ($var) { 295 | /* @var TypeMatcher $matcher */ 296 | $matcher = $record['matcher']; 297 | 298 | return $matcher->matches($var); 299 | } 300 | ); 301 | 302 | return isset($matched) ? $matched['filter'] : null; 303 | } 304 | 305 | /** 306 | * Returns first element that matches predicate, `null` if no such element found. 307 | * 308 | * @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs. 309 | * @param callable $predicate Predicate arguments are: element. 310 | * 311 | * @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher' 312 | * with value of type {@see TypeMatcher} or `null`. 313 | */ 314 | private function first(array $elements, callable $predicate) 315 | { 316 | foreach ($elements as $element) { 317 | if (call_user_func($predicate, $element)) { 318 | return $element; 319 | } 320 | } 321 | 322 | return null; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/DeepCopy/Exception/CloneException.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 18 | } 19 | 20 | public function apply($object, $property, $objectCopier) 21 | { 22 | $this->filter->apply($object, $property, $objectCopier); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DeepCopy/Filter/Doctrine/DoctrineCollectionFilter.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 23 | $oldCollection = $reflectionProperty->getValue($object); 24 | 25 | $newCollection = $oldCollection->map( 26 | function ($item) use ($objectCopier) { 27 | return $objectCopier($item); 28 | } 29 | ); 30 | 31 | $reflectionProperty->setValue($object, $newCollection); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DeepCopy/Filter/Doctrine/DoctrineEmptyCollectionFilter.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 25 | 26 | $reflectionProperty->setValue($object, new ArrayCollection()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/DeepCopy/Filter/Doctrine/DoctrineProxyFilter.php: -------------------------------------------------------------------------------- 1 | __load(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DeepCopy/Filter/Filter.php: -------------------------------------------------------------------------------- 1 | callback = $callable; 23 | } 24 | 25 | /** 26 | * Replaces the object property by the result of the callback called with the object property. 27 | * 28 | * {@inheritdoc} 29 | */ 30 | public function apply($object, $property, $objectCopier) 31 | { 32 | $reflectionProperty = ReflectionHelper::getProperty($object, $property); 33 | $reflectionProperty->setAccessible(true); 34 | 35 | $value = call_user_func($this->callback, $reflectionProperty->getValue($object)); 36 | 37 | $reflectionProperty->setValue($object, $value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DeepCopy/Filter/SetNullFilter.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 22 | $reflectionProperty->setValue($object, null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php: -------------------------------------------------------------------------------- 1 | class = $class; 27 | $this->property = $property; 28 | } 29 | 30 | /** 31 | * Matches a specific property of a specific class. 32 | * 33 | * {@inheritdoc} 34 | */ 35 | public function matches($object, $property) 36 | { 37 | return ($object instanceof $this->class) && $property == $this->property; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DeepCopy/Matcher/PropertyNameMatcher.php: -------------------------------------------------------------------------------- 1 | property = $property; 21 | } 22 | 23 | /** 24 | * Matches a property by its name. 25 | * 26 | * {@inheritdoc} 27 | */ 28 | public function matches($object, $property) 29 | { 30 | return $property == $this->property; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DeepCopy/Matcher/PropertyTypeMatcher.php: -------------------------------------------------------------------------------- 1 | propertyType = $propertyType; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function matches($object, $property) 35 | { 36 | try { 37 | $reflectionProperty = ReflectionHelper::getProperty($object, $property); 38 | } catch (ReflectionException $exception) { 39 | return false; 40 | } 41 | 42 | $reflectionProperty->setAccessible(true); 43 | 44 | // Uninitialized properties (for PHP >7.4) 45 | if (method_exists($reflectionProperty, 'isInitialized') && !$reflectionProperty->isInitialized($object)) { 46 | // null instanceof $this->propertyType 47 | return false; 48 | } 49 | 50 | return $reflectionProperty->getValue($object) instanceof $this->propertyType; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DeepCopy/Reflection/ReflectionHelper.php: -------------------------------------------------------------------------------- 1 | getProperties() does not return private properties from ancestor classes. 17 | * 18 | * @author muratyaman@gmail.com 19 | * @see http://php.net/manual/en/reflectionclass.getproperties.php 20 | * 21 | * @param ReflectionClass $ref 22 | * 23 | * @return ReflectionProperty[] 24 | */ 25 | public static function getProperties(ReflectionClass $ref) 26 | { 27 | $props = $ref->getProperties(); 28 | $propsArr = array(); 29 | 30 | foreach ($props as $prop) { 31 | $propertyName = $prop->getName(); 32 | $propsArr[$propertyName] = $prop; 33 | } 34 | 35 | if ($parentClass = $ref->getParentClass()) { 36 | $parentPropsArr = self::getProperties($parentClass); 37 | foreach ($propsArr as $key => $property) { 38 | $parentPropsArr[$key] = $property; 39 | } 40 | 41 | return $parentPropsArr; 42 | } 43 | 44 | return $propsArr; 45 | } 46 | 47 | /** 48 | * Retrieves property by name from object and all its ancestors. 49 | * 50 | * @param object|string $object 51 | * @param string $name 52 | * 53 | * @throws PropertyException 54 | * @throws ReflectionException 55 | * 56 | * @return ReflectionProperty 57 | */ 58 | public static function getProperty($object, $name) 59 | { 60 | $reflection = is_object($object) ? new ReflectionObject($object) : new ReflectionClass($object); 61 | 62 | if ($reflection->hasProperty($name)) { 63 | return $reflection->getProperty($name); 64 | } 65 | 66 | if ($parentClass = $reflection->getParentClass()) { 67 | return self::getProperty($parentClass->getName(), $name); 68 | } 69 | 70 | throw new PropertyException( 71 | sprintf( 72 | 'The class "%s" doesn\'t have a property with the given name: "%s".', 73 | is_object($object) ? get_class($object) : $object, 74 | $name 75 | ) 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/Date/DateIntervalFilter.php: -------------------------------------------------------------------------------- 1 | $propertyValue) { 28 | $copy->{$propertyName} = $propertyValue; 29 | } 30 | 31 | return $copy; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/Date/DatePeriodFilter.php: -------------------------------------------------------------------------------- 1 | = 80200 && $element->include_end_date) { 24 | $options |= DatePeriod::INCLUDE_END_DATE; 25 | } 26 | if (!$element->include_start_date) { 27 | $options |= DatePeriod::EXCLUDE_START_DATE; 28 | } 29 | 30 | if ($element->getEndDate()) { 31 | return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $element->getEndDate(), $options); 32 | } 33 | 34 | if (PHP_VERSION_ID >= 70217) { 35 | $recurrences = $element->getRecurrences(); 36 | } else { 37 | $recurrences = $element->recurrences - $element->include_start_date; 38 | } 39 | 40 | return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $recurrences, $options); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/ReplaceFilter.php: -------------------------------------------------------------------------------- 1 | callback = $callable; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function apply($element) 27 | { 28 | return call_user_func($this->callback, $element); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/ShallowCopyFilter.php: -------------------------------------------------------------------------------- 1 | copier = $copier; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function apply($arrayObject) 27 | { 28 | $clone = clone $arrayObject; 29 | foreach ($arrayObject->getArrayCopy() as $k => $v) { 30 | $clone->offsetSet($k, $this->copier->copy($v)); 31 | } 32 | 33 | return $clone; 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/Spl/SplDoublyLinkedList.php: -------------------------------------------------------------------------------- 1 | copier = $copier; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function apply($element) 26 | { 27 | $newElement = clone $element; 28 | 29 | $copy = $this->createCopyClosure(); 30 | 31 | return $copy($newElement); 32 | } 33 | 34 | private function createCopyClosure() 35 | { 36 | $copier = $this->copier; 37 | 38 | $copy = function (SplDoublyLinkedList $list) use ($copier) { 39 | // Replace each element in the list with a deep copy of itself 40 | for ($i = 1; $i <= $list->count(); $i++) { 41 | $copy = $copier->recursiveCopy($list->shift()); 42 | 43 | $list->push($copy); 44 | } 45 | 46 | return $list; 47 | }; 48 | 49 | return Closure::bind($copy, null, DeepCopy::class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DeepCopy/TypeFilter/TypeFilter.php: -------------------------------------------------------------------------------- 1 | type = $type; 18 | } 19 | 20 | /** 21 | * @param mixed $element 22 | * 23 | * @return boolean 24 | */ 25 | public function matches($element) 26 | { 27 | return is_object($element) ? is_a($element, $this->type) : gettype($element) === $this->type; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DeepCopy/deep_copy.php: -------------------------------------------------------------------------------- 1 | copy($value); 19 | } 20 | } 21 | --------------------------------------------------------------------------------