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