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

VarDumper::dumpInternal()   D

Complexity

Conditions 20
Paths 22

Size

Total Lines 66
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 58.9343

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 20
eloc 51
c 2
b 0
f 0
nc 22
nop 3
dl 0
loc 66
ccs 27
cts 50
cp 0.54
crap 58.9343
rs 4.1666

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
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