DeepCopy.php 8.0 KB

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