comparison vendor/myclabs/deep-copy/src/DeepCopy/DeepCopy.php @ 2:5311817fb629

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