Mercurial > hg > cmmr2012-drupal-site
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 } |