vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php line 163

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