1*dc4d9dc6SAnna Dabrowska<?php 2*dc4d9dc6SAnna Dabrowska 3*dc4d9dc6SAnna Dabrowskanamespace DeepCopy; 4*dc4d9dc6SAnna Dabrowska 5*dc4d9dc6SAnna Dabrowskause ArrayObject; 6*dc4d9dc6SAnna Dabrowskause DateInterval; 7*dc4d9dc6SAnna Dabrowskause DateTimeInterface; 8*dc4d9dc6SAnna Dabrowskause DateTimeZone; 9*dc4d9dc6SAnna Dabrowskause DeepCopy\Exception\CloneException; 10*dc4d9dc6SAnna Dabrowskause DeepCopy\Filter\Filter; 11*dc4d9dc6SAnna Dabrowskause DeepCopy\Matcher\Matcher; 12*dc4d9dc6SAnna Dabrowskause DeepCopy\Reflection\ReflectionHelper; 13*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Date\DateIntervalFilter; 14*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Spl\ArrayObjectFilter; 15*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\Spl\SplDoublyLinkedListFilter; 16*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeFilter\TypeFilter; 17*dc4d9dc6SAnna Dabrowskause DeepCopy\TypeMatcher\TypeMatcher; 18*dc4d9dc6SAnna Dabrowskause ReflectionObject; 19*dc4d9dc6SAnna Dabrowskause ReflectionProperty; 20*dc4d9dc6SAnna Dabrowskause SplDoublyLinkedList; 21*dc4d9dc6SAnna Dabrowska 22*dc4d9dc6SAnna Dabrowska/** 23*dc4d9dc6SAnna Dabrowska * @final 24*dc4d9dc6SAnna Dabrowska */ 25*dc4d9dc6SAnna Dabrowskaclass DeepCopy 26*dc4d9dc6SAnna Dabrowska{ 27*dc4d9dc6SAnna Dabrowska /** 28*dc4d9dc6SAnna Dabrowska * @var object[] List of objects copied. 29*dc4d9dc6SAnna Dabrowska */ 30*dc4d9dc6SAnna Dabrowska private $hashMap = []; 31*dc4d9dc6SAnna Dabrowska 32*dc4d9dc6SAnna Dabrowska /** 33*dc4d9dc6SAnna Dabrowska * Filters to apply. 34*dc4d9dc6SAnna Dabrowska * 35*dc4d9dc6SAnna Dabrowska * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs. 36*dc4d9dc6SAnna Dabrowska */ 37*dc4d9dc6SAnna Dabrowska private $filters = []; 38*dc4d9dc6SAnna Dabrowska 39*dc4d9dc6SAnna Dabrowska /** 40*dc4d9dc6SAnna Dabrowska * Type Filters to apply. 41*dc4d9dc6SAnna Dabrowska * 42*dc4d9dc6SAnna Dabrowska * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs. 43*dc4d9dc6SAnna Dabrowska */ 44*dc4d9dc6SAnna Dabrowska private $typeFilters = []; 45*dc4d9dc6SAnna Dabrowska 46*dc4d9dc6SAnna Dabrowska /** 47*dc4d9dc6SAnna Dabrowska * @var bool 48*dc4d9dc6SAnna Dabrowska */ 49*dc4d9dc6SAnna Dabrowska private $skipUncloneable = false; 50*dc4d9dc6SAnna Dabrowska 51*dc4d9dc6SAnna Dabrowska /** 52*dc4d9dc6SAnna Dabrowska * @var bool 53*dc4d9dc6SAnna Dabrowska */ 54*dc4d9dc6SAnna Dabrowska private $useCloneMethod; 55*dc4d9dc6SAnna Dabrowska 56*dc4d9dc6SAnna Dabrowska /** 57*dc4d9dc6SAnna Dabrowska * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used 58*dc4d9dc6SAnna Dabrowska * instead of the regular deep cloning. 59*dc4d9dc6SAnna Dabrowska */ 60*dc4d9dc6SAnna Dabrowska public function __construct($useCloneMethod = false) 61*dc4d9dc6SAnna Dabrowska { 62*dc4d9dc6SAnna Dabrowska $this->useCloneMethod = $useCloneMethod; 63*dc4d9dc6SAnna Dabrowska 64*dc4d9dc6SAnna Dabrowska $this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class)); 65*dc4d9dc6SAnna Dabrowska $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class)); 66*dc4d9dc6SAnna Dabrowska $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class)); 67*dc4d9dc6SAnna Dabrowska } 68*dc4d9dc6SAnna Dabrowska 69*dc4d9dc6SAnna Dabrowska /** 70*dc4d9dc6SAnna Dabrowska * If enabled, will not throw an exception when coming across an uncloneable property. 71*dc4d9dc6SAnna Dabrowska * 72*dc4d9dc6SAnna Dabrowska * @param $skipUncloneable 73*dc4d9dc6SAnna Dabrowska * 74*dc4d9dc6SAnna Dabrowska * @return $this 75*dc4d9dc6SAnna Dabrowska */ 76*dc4d9dc6SAnna Dabrowska public function skipUncloneable($skipUncloneable = true) 77*dc4d9dc6SAnna Dabrowska { 78*dc4d9dc6SAnna Dabrowska $this->skipUncloneable = $skipUncloneable; 79*dc4d9dc6SAnna Dabrowska 80*dc4d9dc6SAnna Dabrowska return $this; 81*dc4d9dc6SAnna Dabrowska } 82*dc4d9dc6SAnna Dabrowska 83*dc4d9dc6SAnna Dabrowska /** 84*dc4d9dc6SAnna Dabrowska * Deep copies the given object. 85*dc4d9dc6SAnna Dabrowska * 86*dc4d9dc6SAnna Dabrowska * @param mixed $object 87*dc4d9dc6SAnna Dabrowska * 88*dc4d9dc6SAnna Dabrowska * @return mixed 89*dc4d9dc6SAnna Dabrowska */ 90*dc4d9dc6SAnna Dabrowska public function copy($object) 91*dc4d9dc6SAnna Dabrowska { 92*dc4d9dc6SAnna Dabrowska $this->hashMap = []; 93*dc4d9dc6SAnna Dabrowska 94*dc4d9dc6SAnna Dabrowska return $this->recursiveCopy($object); 95*dc4d9dc6SAnna Dabrowska } 96*dc4d9dc6SAnna Dabrowska 97*dc4d9dc6SAnna Dabrowska public function addFilter(Filter $filter, Matcher $matcher) 98*dc4d9dc6SAnna Dabrowska { 99*dc4d9dc6SAnna Dabrowska $this->filters[] = [ 100*dc4d9dc6SAnna Dabrowska 'matcher' => $matcher, 101*dc4d9dc6SAnna Dabrowska 'filter' => $filter, 102*dc4d9dc6SAnna Dabrowska ]; 103*dc4d9dc6SAnna Dabrowska } 104*dc4d9dc6SAnna Dabrowska 105*dc4d9dc6SAnna Dabrowska public function prependFilter(Filter $filter, Matcher $matcher) 106*dc4d9dc6SAnna Dabrowska { 107*dc4d9dc6SAnna Dabrowska array_unshift($this->filters, [ 108*dc4d9dc6SAnna Dabrowska 'matcher' => $matcher, 109*dc4d9dc6SAnna Dabrowska 'filter' => $filter, 110*dc4d9dc6SAnna Dabrowska ]); 111*dc4d9dc6SAnna Dabrowska } 112*dc4d9dc6SAnna Dabrowska 113*dc4d9dc6SAnna Dabrowska public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher) 114*dc4d9dc6SAnna Dabrowska { 115*dc4d9dc6SAnna Dabrowska $this->typeFilters[] = [ 116*dc4d9dc6SAnna Dabrowska 'matcher' => $matcher, 117*dc4d9dc6SAnna Dabrowska 'filter' => $filter, 118*dc4d9dc6SAnna Dabrowska ]; 119*dc4d9dc6SAnna Dabrowska } 120*dc4d9dc6SAnna Dabrowska 121*dc4d9dc6SAnna Dabrowska private function recursiveCopy($var) 122*dc4d9dc6SAnna Dabrowska { 123*dc4d9dc6SAnna Dabrowska // Matches Type Filter 124*dc4d9dc6SAnna Dabrowska if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) { 125*dc4d9dc6SAnna Dabrowska return $filter->apply($var); 126*dc4d9dc6SAnna Dabrowska } 127*dc4d9dc6SAnna Dabrowska 128*dc4d9dc6SAnna Dabrowska // Resource 129*dc4d9dc6SAnna Dabrowska if (is_resource($var)) { 130*dc4d9dc6SAnna Dabrowska return $var; 131*dc4d9dc6SAnna Dabrowska } 132*dc4d9dc6SAnna Dabrowska 133*dc4d9dc6SAnna Dabrowska // Array 134*dc4d9dc6SAnna Dabrowska if (is_array($var)) { 135*dc4d9dc6SAnna Dabrowska return $this->copyArray($var); 136*dc4d9dc6SAnna Dabrowska } 137*dc4d9dc6SAnna Dabrowska 138*dc4d9dc6SAnna Dabrowska // Scalar 139*dc4d9dc6SAnna Dabrowska if (! is_object($var)) { 140*dc4d9dc6SAnna Dabrowska return $var; 141*dc4d9dc6SAnna Dabrowska } 142*dc4d9dc6SAnna Dabrowska 143*dc4d9dc6SAnna Dabrowska // Object 144*dc4d9dc6SAnna Dabrowska return $this->copyObject($var); 145*dc4d9dc6SAnna Dabrowska } 146*dc4d9dc6SAnna Dabrowska 147*dc4d9dc6SAnna Dabrowska /** 148*dc4d9dc6SAnna Dabrowska * Copy an array 149*dc4d9dc6SAnna Dabrowska * @param array $array 150*dc4d9dc6SAnna Dabrowska * @return array 151*dc4d9dc6SAnna Dabrowska */ 152*dc4d9dc6SAnna Dabrowska private function copyArray(array $array) 153*dc4d9dc6SAnna Dabrowska { 154*dc4d9dc6SAnna Dabrowska foreach ($array as $key => $value) { 155*dc4d9dc6SAnna Dabrowska $array[$key] = $this->recursiveCopy($value); 156*dc4d9dc6SAnna Dabrowska } 157*dc4d9dc6SAnna Dabrowska 158*dc4d9dc6SAnna Dabrowska return $array; 159*dc4d9dc6SAnna Dabrowska } 160*dc4d9dc6SAnna Dabrowska 161*dc4d9dc6SAnna Dabrowska /** 162*dc4d9dc6SAnna Dabrowska * Copies an object. 163*dc4d9dc6SAnna Dabrowska * 164*dc4d9dc6SAnna Dabrowska * @param object $object 165*dc4d9dc6SAnna Dabrowska * 166*dc4d9dc6SAnna Dabrowska * @throws CloneException 167*dc4d9dc6SAnna Dabrowska * 168*dc4d9dc6SAnna Dabrowska * @return object 169*dc4d9dc6SAnna Dabrowska */ 170*dc4d9dc6SAnna Dabrowska private function copyObject($object) 171*dc4d9dc6SAnna Dabrowska { 172*dc4d9dc6SAnna Dabrowska $objectHash = spl_object_hash($object); 173*dc4d9dc6SAnna Dabrowska 174*dc4d9dc6SAnna Dabrowska if (isset($this->hashMap[$objectHash])) { 175*dc4d9dc6SAnna Dabrowska return $this->hashMap[$objectHash]; 176*dc4d9dc6SAnna Dabrowska } 177*dc4d9dc6SAnna Dabrowska 178*dc4d9dc6SAnna Dabrowska $reflectedObject = new ReflectionObject($object); 179*dc4d9dc6SAnna Dabrowska $isCloneable = $reflectedObject->isCloneable(); 180*dc4d9dc6SAnna Dabrowska 181*dc4d9dc6SAnna Dabrowska if (false === $isCloneable) { 182*dc4d9dc6SAnna Dabrowska if ($this->skipUncloneable) { 183*dc4d9dc6SAnna Dabrowska $this->hashMap[$objectHash] = $object; 184*dc4d9dc6SAnna Dabrowska 185*dc4d9dc6SAnna Dabrowska return $object; 186*dc4d9dc6SAnna Dabrowska } 187*dc4d9dc6SAnna Dabrowska 188*dc4d9dc6SAnna Dabrowska throw new CloneException( 189*dc4d9dc6SAnna Dabrowska sprintf( 190*dc4d9dc6SAnna Dabrowska 'The class "%s" is not cloneable.', 191*dc4d9dc6SAnna Dabrowska $reflectedObject->getName() 192*dc4d9dc6SAnna Dabrowska ) 193*dc4d9dc6SAnna Dabrowska ); 194*dc4d9dc6SAnna Dabrowska } 195*dc4d9dc6SAnna Dabrowska 196*dc4d9dc6SAnna Dabrowska $newObject = clone $object; 197*dc4d9dc6SAnna Dabrowska $this->hashMap[$objectHash] = $newObject; 198*dc4d9dc6SAnna Dabrowska 199*dc4d9dc6SAnna Dabrowska if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { 200*dc4d9dc6SAnna Dabrowska return $newObject; 201*dc4d9dc6SAnna Dabrowska } 202*dc4d9dc6SAnna Dabrowska 203*dc4d9dc6SAnna Dabrowska if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) { 204*dc4d9dc6SAnna Dabrowska return $newObject; 205*dc4d9dc6SAnna Dabrowska } 206*dc4d9dc6SAnna Dabrowska 207*dc4d9dc6SAnna Dabrowska foreach (ReflectionHelper::getProperties($reflectedObject) as $property) { 208*dc4d9dc6SAnna Dabrowska $this->copyObjectProperty($newObject, $property); 209*dc4d9dc6SAnna Dabrowska } 210*dc4d9dc6SAnna Dabrowska 211*dc4d9dc6SAnna Dabrowska return $newObject; 212*dc4d9dc6SAnna Dabrowska } 213*dc4d9dc6SAnna Dabrowska 214*dc4d9dc6SAnna Dabrowska private function copyObjectProperty($object, ReflectionProperty $property) 215*dc4d9dc6SAnna Dabrowska { 216*dc4d9dc6SAnna Dabrowska // Ignore static properties 217*dc4d9dc6SAnna Dabrowska if ($property->isStatic()) { 218*dc4d9dc6SAnna Dabrowska return; 219*dc4d9dc6SAnna Dabrowska } 220*dc4d9dc6SAnna Dabrowska 221*dc4d9dc6SAnna Dabrowska // Apply the filters 222*dc4d9dc6SAnna Dabrowska foreach ($this->filters as $item) { 223*dc4d9dc6SAnna Dabrowska /** @var Matcher $matcher */ 224*dc4d9dc6SAnna Dabrowska $matcher = $item['matcher']; 225*dc4d9dc6SAnna Dabrowska /** @var Filter $filter */ 226*dc4d9dc6SAnna Dabrowska $filter = $item['filter']; 227*dc4d9dc6SAnna Dabrowska 228*dc4d9dc6SAnna Dabrowska if ($matcher->matches($object, $property->getName())) { 229*dc4d9dc6SAnna Dabrowska $filter->apply( 230*dc4d9dc6SAnna Dabrowska $object, 231*dc4d9dc6SAnna Dabrowska $property->getName(), 232*dc4d9dc6SAnna Dabrowska function ($object) { 233*dc4d9dc6SAnna Dabrowska return $this->recursiveCopy($object); 234*dc4d9dc6SAnna Dabrowska } 235*dc4d9dc6SAnna Dabrowska ); 236*dc4d9dc6SAnna Dabrowska 237*dc4d9dc6SAnna Dabrowska // If a filter matches, we stop processing this property 238*dc4d9dc6SAnna Dabrowska return; 239*dc4d9dc6SAnna Dabrowska } 240*dc4d9dc6SAnna Dabrowska } 241*dc4d9dc6SAnna Dabrowska 242*dc4d9dc6SAnna Dabrowska $property->setAccessible(true); 243*dc4d9dc6SAnna Dabrowska 244*dc4d9dc6SAnna Dabrowska // Ignore uninitialized properties (for PHP >7.4) 245*dc4d9dc6SAnna Dabrowska if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) { 246*dc4d9dc6SAnna Dabrowska return; 247*dc4d9dc6SAnna Dabrowska } 248*dc4d9dc6SAnna Dabrowska 249*dc4d9dc6SAnna Dabrowska $propertyValue = $property->getValue($object); 250*dc4d9dc6SAnna Dabrowska 251*dc4d9dc6SAnna Dabrowska // Copy the property 252*dc4d9dc6SAnna Dabrowska $property->setValue($object, $this->recursiveCopy($propertyValue)); 253*dc4d9dc6SAnna Dabrowska } 254*dc4d9dc6SAnna Dabrowska 255*dc4d9dc6SAnna Dabrowska /** 256*dc4d9dc6SAnna Dabrowska * Returns first filter that matches variable, `null` if no such filter found. 257*dc4d9dc6SAnna Dabrowska * 258*dc4d9dc6SAnna Dabrowska * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 259*dc4d9dc6SAnna Dabrowska * 'matcher' with value of type {@see TypeMatcher} 260*dc4d9dc6SAnna Dabrowska * @param mixed $var 261*dc4d9dc6SAnna Dabrowska * 262*dc4d9dc6SAnna Dabrowska * @return TypeFilter|null 263*dc4d9dc6SAnna Dabrowska */ 264*dc4d9dc6SAnna Dabrowska private function getFirstMatchedTypeFilter(array $filterRecords, $var) 265*dc4d9dc6SAnna Dabrowska { 266*dc4d9dc6SAnna Dabrowska $matched = $this->first( 267*dc4d9dc6SAnna Dabrowska $filterRecords, 268*dc4d9dc6SAnna Dabrowska function (array $record) use ($var) { 269*dc4d9dc6SAnna Dabrowska /* @var TypeMatcher $matcher */ 270*dc4d9dc6SAnna Dabrowska $matcher = $record['matcher']; 271*dc4d9dc6SAnna Dabrowska 272*dc4d9dc6SAnna Dabrowska return $matcher->matches($var); 273*dc4d9dc6SAnna Dabrowska } 274*dc4d9dc6SAnna Dabrowska ); 275*dc4d9dc6SAnna Dabrowska 276*dc4d9dc6SAnna Dabrowska return isset($matched) ? $matched['filter'] : null; 277*dc4d9dc6SAnna Dabrowska } 278*dc4d9dc6SAnna Dabrowska 279*dc4d9dc6SAnna Dabrowska /** 280*dc4d9dc6SAnna Dabrowska * Returns first element that matches predicate, `null` if no such element found. 281*dc4d9dc6SAnna Dabrowska * 282*dc4d9dc6SAnna Dabrowska * @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs. 283*dc4d9dc6SAnna Dabrowska * @param callable $predicate Predicate arguments are: element. 284*dc4d9dc6SAnna Dabrowska * 285*dc4d9dc6SAnna Dabrowska * @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher' 286*dc4d9dc6SAnna Dabrowska * with value of type {@see TypeMatcher} or `null`. 287*dc4d9dc6SAnna Dabrowska */ 288*dc4d9dc6SAnna Dabrowska private function first(array $elements, callable $predicate) 289*dc4d9dc6SAnna Dabrowska { 290*dc4d9dc6SAnna Dabrowska foreach ($elements as $element) { 291*dc4d9dc6SAnna Dabrowska if (call_user_func($predicate, $element)) { 292*dc4d9dc6SAnna Dabrowska return $element; 293*dc4d9dc6SAnna Dabrowska } 294*dc4d9dc6SAnna Dabrowska } 295*dc4d9dc6SAnna Dabrowska 296*dc4d9dc6SAnna Dabrowska return null; 297*dc4d9dc6SAnna Dabrowska } 298*dc4d9dc6SAnna Dabrowska} 299