Total Complexity | 115 |
Total Lines | 398 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Exporter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Exporter, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
21 | class Exporter |
||
22 | { |
||
23 | /** |
||
24 | * Prepares an array of values for VarExporter. |
||
25 | * |
||
26 | * For performance this method is public and has no type-hints. |
||
27 | * |
||
28 | * @param array &$values |
||
29 | * @param \SplObjectStorage $objectsPool |
||
30 | * @param array &$refsPool |
||
31 | * @param int &$objectsCount |
||
32 | * @param bool &$valuesAreStatic |
||
33 | * |
||
34 | * @return array |
||
35 | * |
||
36 | * @throws NotInstantiableTypeException When a value cannot be serialized |
||
37 | */ |
||
38 | public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic) |
||
39 | { |
||
40 | $refs = $values; |
||
41 | foreach ($values as $k => $value) { |
||
42 | if (\is_resource($value)) { |
||
43 | throw new NotInstantiableTypeException(get_resource_type($value).' resource'); |
||
44 | } |
||
45 | $refs[$k] = $objectsPool; |
||
46 | |||
47 | if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) { |
||
48 | $values[$k] = &$value; // Break hard references to make $values completely |
||
49 | unset($value); // independent from the original structure |
||
50 | $refs[$k] = $value = $values[$k]; |
||
51 | if ($value instanceof Reference && 0 > $value->id) { |
||
52 | $valuesAreStatic = false; |
||
53 | ++$value->count; |
||
54 | continue; |
||
55 | } |
||
56 | $refsPool[] = [&$refs[$k], $value, &$value]; |
||
57 | $refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value); |
||
58 | } |
||
59 | |||
60 | if (\is_array($value)) { |
||
61 | if ($value) { |
||
|
|||
62 | $value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); |
||
63 | } |
||
64 | goto handle_value; |
||
65 | } elseif (!\is_object($value) || $value instanceof \UnitEnum) { |
||
66 | goto handle_value; |
||
67 | } |
||
68 | |||
69 | $valueIsStatic = false; |
||
70 | if (isset($objectsPool[$value])) { |
||
71 | ++$objectsCount; |
||
72 | $value = new Reference($objectsPool[$value][0]); |
||
73 | goto handle_value; |
||
74 | } |
||
75 | |||
76 | $class = $value::class; |
||
77 | $reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class); |
||
78 | $properties = []; |
||
79 | |||
80 | if ($reflector->hasMethod('__serialize')) { |
||
81 | if (!$reflector->getMethod('__serialize')->isPublic()) { |
||
82 | throw new \Error(\sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class)); |
||
83 | } |
||
84 | |||
85 | if (!\is_array($serializeProperties = $value->__serialize())) { |
||
86 | throw new \TypeError($class.'::__serialize() must return an array'); |
||
87 | } |
||
88 | |||
89 | if ($reflector->hasMethod('__unserialize')) { |
||
90 | $properties = $serializeProperties; |
||
91 | } else { |
||
92 | foreach ($serializeProperties as $n => $v) { |
||
93 | $p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; |
||
94 | $c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; |
||
95 | $properties[$c][$n] = $v; |
||
96 | } |
||
97 | } |
||
98 | |||
99 | goto prepare_value; |
||
100 | } |
||
101 | |||
102 | $sleep = null; |
||
103 | $proto = Registry::$prototypes[$class]; |
||
104 | |||
105 | if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) { |
||
106 | // ArrayIterator and ArrayObject need special care because their "flags" |
||
107 | // option changes the behavior of the (array) casting operator. |
||
108 | [$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto); |
||
109 | |||
110 | // populates Registry::$prototypes[$class] with a new instance |
||
111 | Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]); |
||
112 | } elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) { |
||
113 | // By implementing Serializable, SplObjectStorage breaks |
||
114 | // internal references; let's deal with it on our own. |
||
115 | foreach (clone $value as $v) { |
||
116 | $properties[] = $v; |
||
117 | $properties[] = $value[$v]; |
||
118 | } |
||
119 | $properties = ['SplObjectStorage' => ["\0" => $properties]]; |
||
120 | $arrayValue = (array) $value; |
||
121 | } elseif ($value instanceof \Serializable |
||
122 | || $value instanceof \__PHP_Incomplete_Class |
||
123 | ) { |
||
124 | ++$objectsCount; |
||
125 | $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; |
||
126 | $value = new Reference($id); |
||
127 | goto handle_value; |
||
128 | } else { |
||
129 | if (method_exists($class, '__sleep')) { |
||
130 | if (!\is_array($sleep = $value->__sleep())) { |
||
131 | trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE); |
||
132 | $value = null; |
||
133 | goto handle_value; |
||
134 | } |
||
135 | $sleep = array_flip($sleep); |
||
136 | } |
||
137 | |||
138 | $arrayValue = (array) $value; |
||
139 | } |
||
140 | |||
141 | $proto = (array) $proto; |
||
142 | |||
143 | foreach ($arrayValue as $name => $v) { |
||
144 | $i = 0; |
||
145 | $n = (string) $name; |
||
146 | if ('' === $n || "\0" !== $n[0]) { |
||
147 | $p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; |
||
148 | $c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; |
||
149 | } elseif ('*' === $n[1]) { |
||
150 | $n = substr($n, 3); |
||
151 | $c = $reflector->getProperty($n)->class; |
||
152 | if ('Error' === $c) { |
||
153 | $c = 'TypeError'; |
||
154 | } elseif ('Exception' === $c) { |
||
155 | $c = 'ErrorException'; |
||
156 | } |
||
157 | } else { |
||
158 | $i = strpos($n, "\0", 2); |
||
159 | $c = substr($n, 1, $i - 1); |
||
160 | $n = substr($n, 1 + $i); |
||
161 | } |
||
162 | if (null !== $sleep) { |
||
163 | if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) { |
||
164 | unset($arrayValue[$name]); |
||
165 | continue; |
||
166 | } |
||
167 | unset($sleep[$name], $sleep[$n]); |
||
168 | } |
||
169 | if (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) { |
||
170 | $properties[$c][$n] = $v; |
||
171 | } |
||
172 | } |
||
173 | if ($sleep) { |
||
174 | foreach ($sleep as $n => $v) { |
||
175 | trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); |
||
176 | } |
||
177 | } |
||
178 | if (method_exists($class, '__unserialize')) { |
||
179 | $properties = $arrayValue; |
||
180 | } |
||
181 | |||
182 | prepare_value: |
||
183 | $objectsPool[$value] = [$id = \count($objectsPool)]; |
||
184 | $properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); |
||
185 | ++$objectsCount; |
||
186 | $objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)]; |
||
187 | |||
188 | $value = new Reference($id); |
||
189 | |||
190 | handle_value: |
||
191 | if ($isRef) { |
||
192 | unset($value); // Break the hard reference created above |
||
193 | } elseif (!$valueIsStatic) { |
||
194 | $values[$k] = $value; |
||
195 | } |
||
196 | $valuesAreStatic = $valueIsStatic && $valuesAreStatic; |
||
197 | } |
||
198 | |||
199 | return $values; |
||
200 | } |
||
201 | |||
202 | public static function export($value, $indent = '') |
||
203 | { |
||
204 | switch (true) { |
||
205 | case \is_int($value) || \is_float($value): return var_export($value, true); |
||
206 | case [] === $value: return '[]'; |
||
207 | case false === $value: return 'false'; |
||
208 | case true === $value: return 'true'; |
||
209 | case null === $value: return 'null'; |
||
210 | case '' === $value: return "''"; |
||
211 | case $value instanceof \UnitEnum: return '\\'.ltrim(var_export($value, true), '\\'); |
||
212 | } |
||
213 | |||
214 | if ($value instanceof Reference) { |
||
215 | if (0 <= $value->id) { |
||
216 | return '$o['.$value->id.']'; |
||
217 | } |
||
218 | if (!$value->count) { |
||
219 | return self::export($value->value, $indent); |
||
220 | } |
||
221 | $value = -$value->id; |
||
222 | |||
223 | return '&$r['.$value.']'; |
||
224 | } |
||
225 | $subIndent = $indent.' '; |
||
226 | |||
227 | if (\is_string($value)) { |
||
228 | $code = \sprintf("'%s'", addcslashes($value, "'\\")); |
||
229 | |||
230 | $code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", function ($m) use ($subIndent) { |
||
231 | $m[1] = \sprintf('\'."%s".\'', str_replace( |
||
232 | ["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'], |
||
233 | ['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'], |
||
234 | $m[1] |
||
235 | )); |
||
236 | |||
237 | if ("'" === $m[2]) { |
||
238 | return substr($m[1], 0, -2); |
||
239 | } |
||
240 | |||
241 | if (str_ends_with($m[1], 'n".\'')) { |
||
242 | return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); |
||
243 | } |
||
244 | |||
245 | return $m[1].$m[2]; |
||
246 | }, $code, -1, $count); |
||
247 | |||
248 | if ($count && str_starts_with($code, "''.")) { |
||
249 | $code = substr($code, 3); |
||
250 | } |
||
251 | |||
252 | return $code; |
||
253 | } |
||
254 | |||
255 | if (\is_array($value)) { |
||
256 | $j = -1; |
||
257 | $code = ''; |
||
258 | foreach ($value as $k => $v) { |
||
259 | $code .= $subIndent; |
||
260 | if (!\is_int($k) || 1 !== $k - $j) { |
||
261 | $code .= self::export($k, $subIndent).' => '; |
||
262 | } |
||
263 | if (\is_int($k) && $k > $j) { |
||
264 | $j = $k; |
||
265 | } |
||
266 | $code .= self::export($v, $subIndent).",\n"; |
||
267 | } |
||
268 | |||
269 | return "[\n".$code.$indent.']'; |
||
270 | } |
||
271 | |||
272 | if ($value instanceof Values) { |
||
273 | $code = $subIndent."\$r = [],\n"; |
||
274 | foreach ($value->values as $k => $v) { |
||
275 | $code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n"; |
||
276 | } |
||
277 | |||
278 | return "[\n".$code.$indent.']'; |
||
279 | } |
||
280 | |||
281 | if ($value instanceof Registry) { |
||
282 | return self::exportRegistry($value, $indent, $subIndent); |
||
283 | } |
||
284 | |||
285 | if ($value instanceof Hydrator) { |
||
286 | return self::exportHydrator($value, $indent, $subIndent); |
||
287 | } |
||
288 | |||
289 | throw new \UnexpectedValueException(\sprintf('Cannot export value of type "%s".', get_debug_type($value))); |
||
290 | } |
||
291 | |||
292 | private static function exportRegistry(Registry $value, string $indent, string $subIndent): string |
||
362 | } |
||
363 | |||
364 | private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string |
||
365 | { |
||
366 | $code = ''; |
||
367 | foreach ($value->properties as $class => $properties) { |
||
368 | $code .= $subIndent.' '.self::export($class).' => '.self::export($properties, $subIndent.' ').",\n"; |
||
369 | } |
||
370 | |||
371 | $code = [ |
||
372 | self::export($value->registry, $subIndent), |
||
373 | self::export($value->values, $subIndent), |
||
374 | '' !== $code ? "[\n".$code.$subIndent.']' : '[]', |
||
375 | self::export($value->value, $subIndent), |
||
376 | self::export($value->wakeups, $subIndent), |
||
377 | ]; |
||
378 | |||
379 | return '\\'.$value::class."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')'; |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | * @param \ArrayIterator|\ArrayObject $value |
||
384 | * @param \ArrayIterator|\ArrayObject $proto |
||
385 | */ |
||
386 | private static function getArrayObjectProperties($value, $proto): array |
||
419 | } |
||
420 | } |
||
421 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.