annotate vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php @ 19:fa3358dc1485 tip

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