annotate vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php @ 5:12f9dff5fda9 tip

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