DeepCopy.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. * @param mixed $object
  79. *
  80. * @return mixed
  81. */
  82. public function copy($object)
  83. {
  84. $this->hashMap = [];
  85. return $this->recursiveCopy($object);
  86. }
  87. public function addFilter(Filter $filter, Matcher $matcher)
  88. {
  89. $this->filters[] = [
  90. 'matcher' => $matcher,
  91. 'filter' => $filter,
  92. ];
  93. }
  94. public function prependFilter(Filter $filter, Matcher $matcher)
  95. {
  96. array_unshift($this->filters, [
  97. 'matcher' => $matcher,
  98. 'filter' => $filter,
  99. ]);
  100. }
  101. public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
  102. {
  103. $this->typeFilters[] = [
  104. 'matcher' => $matcher,
  105. 'filter' => $filter,
  106. ];
  107. }
  108. private function recursiveCopy($var)
  109. {
  110. // Matches Type Filter
  111. if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
  112. return $filter->apply($var);
  113. }
  114. // Resource
  115. if (is_resource($var)) {
  116. return $var;
  117. }
  118. // Array
  119. if (is_array($var)) {
  120. return $this->copyArray($var);
  121. }
  122. // Scalar
  123. if (! is_object($var)) {
  124. return $var;
  125. }
  126. // Enum
  127. if (PHP_VERSION_ID >= 80100 && enum_exists(get_class($var))) {
  128. return $var;
  129. }
  130. // Object
  131. return $this->copyObject($var);
  132. }
  133. /**
  134. * Copy an array
  135. * @param array $array
  136. * @return array
  137. */
  138. private function copyArray(array $array)
  139. {
  140. foreach ($array as $key => $value) {
  141. $array[$key] = $this->recursiveCopy($value);
  142. }
  143. return $array;
  144. }
  145. /**
  146. * Copies an object.
  147. *
  148. * @param object $object
  149. *
  150. * @throws CloneException
  151. *
  152. * @return object
  153. */
  154. private function copyObject($object)
  155. {
  156. $objectHash = spl_object_hash($object);
  157. if (isset($this->hashMap[$objectHash])) {
  158. return $this->hashMap[$objectHash];
  159. }
  160. $reflectedObject = new ReflectionObject($object);
  161. $isCloneable = $reflectedObject->isCloneable();
  162. if (false === $isCloneable) {
  163. if ($this->skipUncloneable) {
  164. $this->hashMap[$objectHash] = $object;
  165. return $object;
  166. }
  167. throw new CloneException(
  168. sprintf(
  169. 'The class "%s" is not cloneable.',
  170. $reflectedObject->getName()
  171. )
  172. );
  173. }
  174. $newObject = clone $object;
  175. $this->hashMap[$objectHash] = $newObject;
  176. if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
  177. return $newObject;
  178. }
  179. if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) {
  180. return $newObject;
  181. }
  182. foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
  183. $this->copyObjectProperty($newObject, $property);
  184. }
  185. return $newObject;
  186. }
  187. private function copyObjectProperty($object, ReflectionProperty $property)
  188. {
  189. // Ignore static properties
  190. if ($property->isStatic()) {
  191. return;
  192. }
  193. // Ignore readonly properties
  194. if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) {
  195. return;
  196. }
  197. // Apply the filters
  198. foreach ($this->filters as $item) {
  199. /** @var Matcher $matcher */
  200. $matcher = $item['matcher'];
  201. /** @var Filter $filter */
  202. $filter = $item['filter'];
  203. if ($matcher->matches($object, $property->getName())) {
  204. $filter->apply(
  205. $object,
  206. $property->getName(),
  207. function ($object) {
  208. return $this->recursiveCopy($object);
  209. }
  210. );
  211. if ($filter instanceof ChainableFilter) {
  212. continue;
  213. }
  214. // If a filter matches, we stop processing this property
  215. return;
  216. }
  217. }
  218. $property->setAccessible(true);
  219. // Ignore uninitialized properties (for PHP >7.4)
  220. if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) {
  221. return;
  222. }
  223. $propertyValue = $property->getValue($object);
  224. // Copy the property
  225. $property->setValue($object, $this->recursiveCopy($propertyValue));
  226. }
  227. /**
  228. * Returns first filter that matches variable, `null` if no such filter found.
  229. *
  230. * @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
  231. * 'matcher' with value of type {@see TypeMatcher}
  232. * @param mixed $var
  233. *
  234. * @return TypeFilter|null
  235. */
  236. private function getFirstMatchedTypeFilter(array $filterRecords, $var)
  237. {
  238. $matched = $this->first(
  239. $filterRecords,
  240. function (array $record) use ($var) {
  241. /* @var TypeMatcher $matcher */
  242. $matcher = $record['matcher'];
  243. return $matcher->matches($var);
  244. }
  245. );
  246. return isset($matched) ? $matched['filter'] : null;
  247. }
  248. /**
  249. * Returns first element that matches predicate, `null` if no such element found.
  250. *
  251. * @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
  252. * @param callable $predicate Predicate arguments are: element.
  253. *
  254. * @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher'
  255. * with value of type {@see TypeMatcher} or `null`.
  256. */
  257. private function first(array $elements, callable $predicate)
  258. {
  259. foreach ($elements as $element) {
  260. if (call_user_func($predicate, $element)) {
  261. return $element;
  262. }
  263. }
  264. return null;
  265. }
  266. }