Chris@14: Filter, 'matcher' => Matcher] pairs. Chris@14: */ Chris@14: private $filters = []; Chris@14: Chris@14: /** Chris@14: * Type Filters to apply. Chris@14: * Chris@14: * @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs. Chris@14: */ Chris@14: private $typeFilters = []; Chris@14: Chris@14: /** Chris@14: * @var bool Chris@14: */ Chris@14: private $skipUncloneable = false; Chris@14: Chris@14: /** Chris@14: * @var bool Chris@14: */ Chris@14: private $useCloneMethod; Chris@14: Chris@14: /** Chris@14: * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used Chris@14: * instead of the regular deep cloning. Chris@14: */ Chris@14: public function __construct($useCloneMethod = false) Chris@14: { Chris@14: $this->useCloneMethod = $useCloneMethod; Chris@14: Chris@14: $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class)); Chris@14: $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class)); Chris@14: } Chris@14: Chris@14: /** Chris@14: * If enabled, will not throw an exception when coming across an uncloneable property. Chris@14: * Chris@14: * @param $skipUncloneable Chris@14: * Chris@14: * @return $this Chris@14: */ Chris@14: public function skipUncloneable($skipUncloneable = true) Chris@14: { Chris@14: $this->skipUncloneable = $skipUncloneable; Chris@14: Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Deep copies the given object. Chris@14: * Chris@14: * @param mixed $object Chris@14: * Chris@14: * @return mixed Chris@14: */ Chris@14: public function copy($object) Chris@14: { Chris@14: $this->hashMap = []; Chris@14: Chris@14: return $this->recursiveCopy($object); Chris@14: } Chris@14: Chris@14: public function addFilter(Filter $filter, Matcher $matcher) Chris@14: { Chris@14: $this->filters[] = [ Chris@14: 'matcher' => $matcher, Chris@14: 'filter' => $filter, Chris@14: ]; Chris@14: } Chris@14: Chris@14: public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher) Chris@14: { Chris@14: $this->typeFilters[] = [ Chris@14: 'matcher' => $matcher, Chris@14: 'filter' => $filter, Chris@14: ]; Chris@14: } Chris@14: Chris@14: private function recursiveCopy($var) Chris@14: { Chris@14: // Matches Type Filter Chris@14: if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) { Chris@14: return $filter->apply($var); Chris@14: } Chris@14: Chris@14: // Resource Chris@14: if (is_resource($var)) { Chris@14: return $var; Chris@14: } Chris@14: Chris@14: // Array Chris@14: if (is_array($var)) { Chris@14: return $this->copyArray($var); Chris@14: } Chris@14: Chris@14: // Scalar Chris@14: if (! is_object($var)) { Chris@14: return $var; Chris@14: } Chris@14: Chris@14: // Object Chris@14: return $this->copyObject($var); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Copy an array Chris@14: * @param array $array Chris@14: * @return array Chris@14: */ Chris@14: private function copyArray(array $array) Chris@14: { Chris@14: foreach ($array as $key => $value) { Chris@14: $array[$key] = $this->recursiveCopy($value); Chris@14: } Chris@14: Chris@14: return $array; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Copies an object. Chris@14: * Chris@14: * @param object $object Chris@14: * Chris@14: * @throws CloneException Chris@14: * Chris@14: * @return object Chris@14: */ Chris@14: private function copyObject($object) Chris@14: { Chris@14: $objectHash = spl_object_hash($object); Chris@14: Chris@14: if (isset($this->hashMap[$objectHash])) { Chris@14: return $this->hashMap[$objectHash]; Chris@14: } Chris@14: Chris@14: $reflectedObject = new ReflectionObject($object); Chris@14: $isCloneable = $reflectedObject->isCloneable(); Chris@14: Chris@14: if (false === $isCloneable) { Chris@14: if ($this->skipUncloneable) { Chris@14: $this->hashMap[$objectHash] = $object; Chris@14: Chris@14: return $object; Chris@14: } Chris@14: Chris@14: throw new CloneException( Chris@14: sprintf( Chris@14: 'The class "%s" is not cloneable.', Chris@14: $reflectedObject->getName() Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: $newObject = clone $object; Chris@14: $this->hashMap[$objectHash] = $newObject; Chris@14: Chris@14: if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { Chris@14: return $newObject; Chris@14: } Chris@14: Chris@14: if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) { Chris@14: return $newObject; Chris@14: } Chris@14: Chris@14: foreach (ReflectionHelper::getProperties($reflectedObject) as $property) { Chris@14: $this->copyObjectProperty($newObject, $property); Chris@14: } Chris@14: Chris@14: return $newObject; Chris@14: } Chris@14: Chris@14: private function copyObjectProperty($object, ReflectionProperty $property) Chris@14: { Chris@14: // Ignore static properties Chris@14: if ($property->isStatic()) { Chris@14: return; Chris@14: } Chris@14: Chris@14: // Apply the filters Chris@14: foreach ($this->filters as $item) { Chris@14: /** @var Matcher $matcher */ Chris@14: $matcher = $item['matcher']; Chris@14: /** @var Filter $filter */ Chris@14: $filter = $item['filter']; Chris@14: Chris@14: if ($matcher->matches($object, $property->getName())) { Chris@14: $filter->apply( Chris@14: $object, Chris@14: $property->getName(), Chris@14: function ($object) { Chris@14: return $this->recursiveCopy($object); Chris@14: } Chris@14: ); Chris@14: Chris@14: // If a filter matches, we stop processing this property Chris@14: return; Chris@14: } Chris@14: } Chris@14: Chris@14: $property->setAccessible(true); Chris@14: $propertyValue = $property->getValue($object); Chris@14: Chris@14: // Copy the property Chris@14: $property->setValue($object, $this->recursiveCopy($propertyValue)); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns first filter that matches variable, `null` if no such filter found. Chris@14: * Chris@14: * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and Chris@14: * 'matcher' with value of type {@see TypeMatcher} Chris@14: * @param mixed $var Chris@14: * Chris@14: * @return TypeFilter|null Chris@14: */ Chris@14: private function getFirstMatchedTypeFilter(array $filterRecords, $var) Chris@14: { Chris@14: $matched = $this->first( Chris@14: $filterRecords, Chris@14: function (array $record) use ($var) { Chris@14: /* @var TypeMatcher $matcher */ Chris@14: $matcher = $record['matcher']; Chris@14: Chris@14: return $matcher->matches($var); Chris@14: } Chris@14: ); Chris@14: Chris@14: return isset($matched) ? $matched['filter'] : null; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns first element that matches predicate, `null` if no such element found. Chris@14: * Chris@14: * @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs. Chris@14: * @param callable $predicate Predicate arguments are: element. Chris@14: * Chris@14: * @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher' Chris@14: * with value of type {@see TypeMatcher} or `null`. Chris@14: */ Chris@14: private function first(array $elements, callable $predicate) Chris@14: { Chris@14: foreach ($elements as $element) { Chris@14: if (call_user_func($predicate, $element)) { Chris@14: return $element; Chris@14: } Chris@14: } Chris@14: Chris@14: return null; Chris@14: } Chris@14: }