Test Failed
Pull Request — master (#37)
by Alexander
02:25
created

VarDumper::exportInternal()   C

Complexity

Conditions 17
Paths 27

Size

Total Lines 56
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 17.1819

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 17
eloc 37
c 5
b 0
f 0
nc 27
nop 3
dl 0
loc 56
ccs 32
cts 35
cp 0.9143
crap 17.1819
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use Closure;
8
use Exception;
9
use IteratorAggregate;
10
use Yiisoft\Arrays\ArrayableInterface;
11
use function get_class;
12
use function in_array;
13
use function is_array;
14
use function is_object;
15
16
/**
17
 * VarDumper provides enhanced versions of the PHP functions {@see var_dump()} and {@see var_export()}.
18
 *
19
 * It can:
20
 *
21
 * - Correctly identify the recursively referenced objects in a complex object structure.
22
 * - Recursively control depth to avoid indefinite recursive display of some peculiar variables.
23
 * - Export closures and objects.
24
 * - Highlight output.
25
 * - Format output.
26
 */
27
final class VarDumper
28
{
29
    /**
30
     * @var mixed Variable to dump.
31
     */
32
    private $variable;
33
    private array $objects = [];
34
35
    private static ?ClosureExporter $closureExporter = null;
36
37
    /**
38
     * @param mixed $variable Variable to dump.
39 93
     */
40
    private function __construct($variable)
41 93
    {
42 93
        $this->variable = $variable;
43
    }
44
45
    /**
46
     * @param mixed $variable Variable to dump.
47
     *
48
     * @return static An instance containing variable to dump.
49 93
     */
50
    public static function create($variable): self
51 93
    {
52
        return new self($variable);
53
    }
54
55
    /**
56
     * Prints a variable.
57
     *
58
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
59
     * but is more robust when handling complex objects.
60
     *
61
     * @param mixed $variable Variable to be dumped.
62
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
63
     * @param bool $highlight Whether the result should be syntax-highlighted.
64
     */
65
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
66
    {
67
        echo self::create($variable)->asString($depth, $highlight);
68
    }
69
70
    /**
71
     * Dumps a variable in terms of a string.
72
     *
73
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
74
     * but is more robust when handling complex objects.
75
     *
76
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
77
     * @param bool $highlight Whether the result should be syntax-highlighted.
78
     *
79
     * @return string The string representation of the variable.
80 25
     */
81
    public function asString(int $depth = 10, bool $highlight = false): string
82 25
    {
83 25
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
84
        if ($highlight) {
85
            $result = highlight_string("<?php\n" . $output, true);
86
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
87
        }
88 25
89
        return $output;
90
    }
91 25
92
    private function dumpNested($variable, int $depth, int $objectCollapseLevel)
93 25
    {
94 25
        $this->buildObjectsCache($variable, $depth);
95
        return $this->dumpNestedInternal($variable, $depth, 0, $objectCollapseLevel);
96
    }
97
98
    /**
99
     * Export variable information as array.
100
     *
101
     * @param int $depth Maximum depth that the dumper should go into the variable.
102
     *
103
     * @return array Variable information as array.
104
     */
105 23
    public function asArray(int $depth = 50): array
106
    {
107 23
        return $this->asArrayInternal($this->variable, $depth, 0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->asArrayInt...s->variable, $depth, 0) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
108
    }
109
110
    /**
111
     * Export variable topmost items summary as array.
112
     *
113
     * @param int $depth Maximum depth that the dumper should go into the variable.
114
     *
115
     * @return array Array containing topmost items summary.
116
     */
117
    public function asArraySummary(int $depth = 50): array
118 2
    {
119
        $this->buildObjectsCache($this->variable, $depth);
120 2
121
        return $this->asArrayInternal($this->objects, $depth, 1);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->asArrayInt...is->objects, $depth, 1) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
122 2
    }
123
124
    /**
125
     * Exports a variable as a string containing PHP code.
126
     *
127
     * The string is a valid PHP expression that can be evaluated by PHP parser
128
     * and the evaluation result will give back the variable value.
129
     *
130
     * This method is similar to {@see var_export()}. The main difference is that
131
     * it generates more compact string representation using short array syntax.
132
     *
133
     * It also handles closures with {@see ClosureExporter} and objects
134
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
135
     *
136
     * @param bool $format Whatever to format code.
137
     *
138
     * @throws \ReflectionException
139
     *
140
     * @return string A PHP code representation of the variable.
141
     */
142
    public function export(bool $format = true): string
143 44
    {
144
        return $this->exportInternal($this->variable, $format, 0);
145 44
    }
146
147
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
148 25
    {
149
        if ($depth <= $level) {
150 25
            return;
151
        }
152
        if (is_object($variable)) {
153 25
            if (in_array($variable, $this->objects, true)) {
154 13
                return;
155 12
            }
156
            $this->objects[] = $variable;
157 13
            $variable = $this->getObjectProperties($variable);
158 13
        }
159
        if (is_array($variable)) {
160 25
            foreach ($variable as $value) {
161 16
                $this->buildObjectsCache($value, $depth, $level + 1);
162 14
            }
163
        }
164
    }
165 25
166
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
167 25
    {
168
        $output = $var;
169 25
170
        switch (gettype($var)) {
171 25
            case 'array':
172 25
                if ($depth <= $level) {
173 6
                    return 'array [...]';
174
                }
175
176
                $output = [];
177 6
                foreach ($var as $key => $value) {
178 6
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
179 5
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
180 5
                }
181
182
                break;
183 6
            case 'object':
184 24
                $objectDescription = $this->getObjectDescription($var);
185 13
                if ($depth <= $level) {
186 13
                    $output = $objectDescription . ' (...)';
187
                    break;
188
                }
189
190
                if ($var instanceof Closure) {
191 13
                    $output = [$objectDescription => $this->exportClosure($var)];
192 10
                    break;
193 10
                }
194
195
                if ($objectCollapseLevel < $level && in_array($var, $this->objects, true)) {
196 4
                    $output = 'object@' . $objectDescription;
197 1
                    break;
198 1
                }
199
200
                $output = [];
201 4
                $properties = $this->getObjectProperties($var);
202 4
                if (empty($properties)) {
203 4
                    $output[$objectDescription] = '{stateless object}';
204 1
                    break;
205 1
                }
206
                foreach ($properties as $key => $value) {
207 3
                    $keyDisplay = $this->normalizeProperty((string) $key);
208 3
                    /**
209
                     * @psalm-suppress InvalidArrayOffset
210
                     */
211
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
212 3
                        $value,
213 3
                        $depth,
214 3
                        $level + 1,
215 3
                        $objectCollapseLevel
216 3
                    );
217
                }
218
219
                break;
220 3
            case 'resource':
221 13
            case 'resource (closed)':
222 12
                $output = $this->getResourceDescription($var);
223 1
                break;
224 1
        }
225
226
        return $output;
227 25
    }
228
229
    private function normalizeProperty(string $property): string
230 3
    {
231
        $property = str_replace("\0", '::', trim($property));
232 3
233
        if (strpos($property, '*::') === 0) {
234 3
            return 'protected $' . substr($property, 3);
235
        }
236
237
        if (($pos = strpos($property, '::')) !== false) {
238 3
            return 'private $' . substr($property, $pos + 2);
239
        }
240
241
        return 'public $' . $property;
242 3
    }
243
244
    /**
245
     * @param mixed $var Variable to be dumped.
246
     * @param bool $format Whatever to format code.
247
     * @param int $depth Maximum depth.
248
     * @param int $level Current depth.
249
     *
250
     * @throws \ReflectionException
251
     *
252
     * @return string
253
     */
254
    private function dumpInternal($var, bool $format, int $depth, int $level): string
255 25
    {
256
        $type = gettype($var);
257 25
        switch ($type) {
258 25
            case 'resource':
259 25
            case 'resource (closed)':
260 24
                return '{resource}';
261 1
            case 'NULL':
262 24
                return 'null';
263 1
            case 'array':
264 23
                if ($depth <= $level) {
265 4
                    return '[...]';
266
                }
267
268
                if (empty($var)) {
269 4
                    return '[]';
270 1
                }
271
272
                $output = '';
273 3
                $keys = array_keys($var);
274 3
                $spaces = str_repeat(' ', $level * 4);
275 3
                $output .= '[';
276 3
                foreach ($keys as $name) {
277 3
                    if ($format) {
278 3
                        $output .= "\n" . $spaces . '    ';
279 3
                    }
280
                    $output .= $this->exportVariable($name);
281 3
                    $output .= ' => ';
282 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
283 3
                }
284
285
                return $format
286 3
                    ? $output . "\n" . $spaces . ']'
287 3
                    : $output . ']';
288 3
            case 'object':
289 22
                if ($var instanceof Closure) {
290 14
                    return $this->exportClosure($var);
291 11
                }
292
                if ($depth <= $level) {
293 5
                    return $this->getObjectDescription($var) . ' (...)';
294
                }
295
296
                $this->objects[] = $var;
297 5
                $spaces = str_repeat(' ', $level * 4);
298 5
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
299 5
                $objectProperties = $this->getObjectProperties($var);
300 5
                foreach ($objectProperties as $name => $value) {
301 5
                    $propertyName = strtr(trim((string) $name), "\0", '::');
302 4
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
303 4
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
304 4
                }
305
                return $output . "\n" . $spaces . ')';
306 5
            default:
307
                return $this->exportVariable($var);
308 10
        }
309
    }
310
311
    private function getObjectProperties($var): array
312 18
    {
313
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
314 18
            $var = $var->__debugInfo();
315 1
        }
316
317
        return (array)$var;
318 18
    }
319
320
    private function getResourceDescription($resource)
321 1
    {
322
        $type = get_resource_type($resource);
323 1
        if ($type === 'stream') {
324 1
            $desc = stream_get_meta_data($resource);
325 1
        } else {
326
            $desc = '{resource}';
327
        }
328
329
        return $desc;
330 1
    }
331
332
    /**
333
     * @param mixed $variable Variable to be exported.
334
     * @param bool $format Whatever to format code.
335
     * @param int $level Current depth.
336
     *
337
     * @throws \ReflectionException
338
     *
339
     * @return string
340
     */
341
    private function exportInternal($variable, bool $format, int $level): string
342 44
    {
343
        switch (gettype($variable)) {
344 44
            case 'NULL':
345 44
                return 'null';
346 2
            case 'array':
347 42
                if (empty($variable)) {
348 8
                    return '[]';
349 2
                }
350
351
                $keys = array_keys($variable);
352 6
                $outputKeys = ($keys !== range(0, count($variable) - 1));
353 6
                $spaces = str_repeat(' ', $level * 4);
354 6
                $output = '[';
355 6
                foreach ($keys as $key) {
356 6
                    if ($format) {
357 6
                        $output .= "\n" . $spaces . '    ';
358 3
                    }
359
                    if ($outputKeys) {
360 6
                        $output .= $this->exportVariable($key);
361 2
                        $output .= ' => ';
362 2
                    }
363
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
364 6
                    if ($format || next($keys) !== false) {
365 6
                        $output .= ',';
366 5
                    }
367
                }
368
                return $format
369 6
                    ? $output . "\n" . $spaces . ']'
370 3
                    : $output . ']';
371 6
            case 'object':
372 40
                if ($variable instanceof Closure) {
373 23
                    return $this->exportClosure($variable);
374 18
                }
375
376
                try {
377
                    return 'unserialize(' . $this->exportVariable(serialize($variable)) . ')';
378 5
                } catch (Exception $e) {
379 1
                    // Serialize may fail, for example: if object contains a `\Closure` instance
380
                    // so we use a fallback.
381
                    if ($variable instanceof ArrayableInterface) {
382 1
                        return $this->exportInternal($variable->toArray(), $format, $level);
383
                    }
384
385
                    if ($variable instanceof IteratorAggregate) {
386 1
                        return $this->exportInternal(iterator_to_array($variable), $format, $level);
387
                    }
388
389
                    if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
390 1
                        return $this->exportVariable($variable->__toString());
391
                    }
392
393
                    return $this->exportVariable(self::create($variable)->asString());
394 1
                }
395
            default:
396
                return $this->exportVariable($variable);
397 17
        }
398
    }
399
400
    /**
401
     * Exports a {@see \Closure} instance.
402
     *
403
     * @param Closure $closure Closure instance.
404
     *
405
     * @throws \ReflectionException
406
     *
407
     * @return string
408
     */
409
    private function exportClosure(Closure $closure): string
410 39
    {
411
        if (self::$closureExporter === null) {
412 39
            self::$closureExporter = new ClosureExporter();
413 1
        }
414
415
        return self::$closureExporter->export($closure);
0 ignored issues
show
Bug introduced by
The method export() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

415
        return self::$closureExporter->/** @scrutinizer ignore-call */ export($closure);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
416 39
    }
417
418
    private function getObjectDescription(object $object): string
419 18
    {
420
        return get_class($object) . '#' . spl_object_id($object);
421 18
    }
422
423
    private function exportVariable($variable): string
424 33
    {
425
        return var_export($variable, true);
426 33
    }
427
428
    private function asArrayInternal($variable, int $depth, int $objectCollapseLevel)
429 25
    {
430
        return $this->dumpNested($variable, $depth, $objectCollapseLevel);
431 25
    }
432
}
433