Passed
Pull Request — master (#19)
by Dmitriy
01:24
created

VarDumper   F

Complexity

Total Complexity 110

Size/Duplication

Total Lines 484
Duplicated Lines 0 %

Test Coverage

Coverage 79.92%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 249
c 4
b 1
f 0
dl 0
loc 484
ccs 207
cts 259
cp 0.7992
rs 2
wmc 110

21 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 3 1
A __construct() 0 3 1
B buildVarObjectsCache() 0 17 8
A asJson() 0 9 2
A dump() 0 3 1
A asString() 0 10 2
A asArray() 0 4 1
A export() 0 3 1
C dumpNestedInternal() 0 61 13
A normalizeProperty() 0 13 3
A asPhpString() 0 5 1
D exportInternal() 0 60 18
A getUsesParser() 0 7 2
A asJsonObjectsMap() 0 15 2
A getVarDumpValuesArray() 0 11 4
A isNextTokenIsPartOfNamespace() 0 7 3
A getObjectsMap() 0 11 3
D dumpInternal() 0 66 20
D exportClosure() 0 60 21
A getObjectDescription() 0 3 1
A getResourceDescription() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like VarDumper 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 VarDumper, and based on these observations, apply Extract Interface, too.

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

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

428
        $source = implode('', array_slice(/** @scrutinizer ignore-type */ file($fileName), $start, $end - $start));
Loading history...
429 11
        $tokens = token_get_all('<?php ' . $source);
430 11
        array_shift($tokens);
431
432 11
        $closureTokens = [];
433 11
        $pendingParenthesisCount = 0;
434 11
        $isShortClosure = false;
435 11
        $buffer = '';
436 11
        foreach ($tokens as $token) {
437 11
            if (!isset($token[0])) {
438
                continue;
439
            }
440 11
            if (in_array($token[0], $this->exportClosureTokens, true)) {
441 11
                $closureTokens[] = $token[1];
442 11
                if (!$isShortClosure && $token[0] === T_FN) {
443 8
                    $isShortClosure = true;
444
                }
445 11
                continue;
446
            }
447 11
            if ($closureTokens !== []) {
448 11
                $readableToken = $token[1] ?? $token;
449 11
                if ($this->isNextTokenIsPartOfNamespace($token)) {
450 6
                    $buffer .= $token[1];
451 6
                    if (!$this->isNextTokenIsPartOfNamespace(next($tokens)) && array_key_exists($buffer, $uses)) {
452 3
                        $readableToken = $uses[$buffer];
453 3
                        $buffer = '';
454
                    }
455
                }
456 11
                if ($token === '{' || $token === '[') {
457 4
                    $pendingParenthesisCount++;
458 11
                } elseif ($token === '}' || $token === ']') {
459 5
                    if ($pendingParenthesisCount === 0) {
460 1
                        break;
461
                    }
462 4
                    $pendingParenthesisCount--;
463 11
                } elseif ($token === ',' || $token === ';') {
464 10
                    if ($pendingParenthesisCount === 0) {
465 10
                        break;
466
                    }
467
                }
468 11
                $closureTokens[] = $readableToken;
469
            }
470
        }
471
472 11
        return implode('', $closureTokens);
473
    }
474
475 9
    public function asPhpString(): string
476
    {
477 9
        $this->exportClosureTokens = [T_FUNCTION, T_FN, T_STATIC];
478 9
        $this->beautify = false;
479 9
        return $this->export();
480
    }
481
482 11
    private function getUsesParser(): UseStatementParser
483
    {
484 11
        if ($this->useStatementParser === null) {
485 11
            $this->useStatementParser = new UseStatementParser();
486
        }
487
488 11
        return $this->useStatementParser;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->useStatementParser could return the type null which is incompatible with the type-hinted return Yiisoft\VarDumper\UseStatementParser. Consider adding an additional type-check to rule them out.
Loading history...
489
    }
490
491 11
    private function isNextTokenIsPartOfNamespace($token): bool
492
    {
493 11
        if (!is_array($token)) {
494 11
            return false;
495
        }
496
497 11
        return $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR;
498
    }
499
500 5
    private function getObjectDescription(object $object): string
501
    {
502 5
        return get_class($object) . '#' . spl_object_id($object);
503
    }
504
}
505