Passed
Pull Request — master (#23)
by Dmitriy
01:57
created

VarDumper::dumpNestedInternal()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 58
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 12.0247

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 37
c 2
b 0
f 0
nc 9
nop 4
dl 0
loc 58
ccs 34
cts 36
cp 0.9444
crap 12.0247
rs 6.9666

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 89
    private function __construct($variable)
31
    {
32 89
        $this->variable = $variable;
33 89
    }
34
35 89
    public static function create($variable): self
36
    {
37 89
        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 22
    public function asString(int $depth = 10, bool $highlight = false): string
62
    {
63 22
        $output = '';
64 22
        $output .= $this->dumpInternal($this->variable, $depth, 0);
65 22
        if ($highlight) {
66
            $result = highlight_string("<?php\n" . $output, true);
67
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
68
        }
69
70 22
        return $output;
71
    }
72
73 25
    private function dumpNested(int $depth, int $objectCollapseLevel = 0)
74
    {
75 25
        $this->buildVarObjectsCache($this->variable, $depth);
76 25
        return $this->dumpNestedInternal($this->variable, $depth, 0, $objectCollapseLevel);
77
    }
78
79 23
    public function asJson(int $depth = 50, bool $prettyPrint = false): string
80
    {
81 23
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
82
83 23
        if ($prettyPrint) {
84
            $options |= JSON_PRETTY_PRINT;
85
        }
86
87 23
        return json_encode($this->dumpNested($depth), $options);
88
    }
89
90 2
    public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string
91
    {
92 2
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
93
94 2
        if ($prettyPrint) {
95
            $options |= JSON_PRETTY_PRINT;
96
        }
97
98 2
        $this->buildVarObjectsCache($this->variable, $depth);
99
100 2
        $backup = $this->variable;
101 2
        $this->variable = self::$objects;
102 2
        $output = json_encode($this->dumpNested($depth, 1), $options);
103 2
        $this->variable = $backup;
104 2
        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 42
    public function export(): string
123
    {
124 42
        return $this->exportInternal($this->variable, 0);
125
    }
126
127 25
    private function buildVarObjectsCache($var, int $depth, int $level = 0): void
128
    {
129 25
        if (is_array($var)) {
130 8
            if ($depth <= $level) {
131
                return;
132
            }
133 8
            foreach ($var as $key => $value) {
134 7
                $this->buildVarObjectsCache($value, $depth, $level + 1);
135
            }
136 24
        } elseif (is_object($var)) {
137 13
            if ($depth <= $level || in_array($var, self::$objects, true)) {
138 12
                return;
139
            }
140 13
            self::$objects[] = $var;
141 13
            $dumpValues = $this->getVarDumpValuesArray($var);
142 13
            foreach ($dumpValues as $key => $value) {
143 12
                $this->buildVarObjectsCache($value, $depth, $level + 1);
144
            }
145
        }
146 25
    }
147
148 25
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
149
    {
150 25
        $output = $var;
151
152 25
        switch (gettype($var)) {
153 25
            case 'array':
154 8
                if ($depth <= $level) {
155
                    return 'array [...]';
156
                }
157
158 8
                $output = [];
159 8
                foreach ($var as $key => $value) {
160 7
                    $keyDisplay = str_replace("\0", '::', trim($key));
161 7
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
162
                }
163
164 8
                break;
165 24
            case 'object':
166 13
                $className = get_class($var);
167
                /**
168
                 * @psalm-var array<string, array<string, array|string>> $output
169
                 */
170 13
                if (($objectCollapseLevel < $level) && (in_array($var, self::$objects, true))) {
171 11
                    if ($var instanceof \Closure) {
172 10
                        $output = $this->exportClosure($var);
173
                    } else {
174 11
                        $output = 'object@' . $this->getObjectDescription($var);
175
                    }
176 12
                } elseif ($depth <= $level) {
177
                    $output = $className . ' (...)';
178
                } else {
179 12
                    $output = [];
180 12
                    $mainKey = $this->getObjectDescription($var);
181 12
                    $dumpValues = $this->getVarDumpValuesArray($var);
182 12
                    if (empty($dumpValues)) {
183 1
                        $output[$mainKey] = '{stateless object}';
184
                    }
185 12
                    foreach ($dumpValues as $key => $value) {
186 11
                        $keyDisplay = $this->normalizeProperty($key);
187
                        /**
188
                         * @psalm-suppress InvalidArrayOffset
189
                         */
190 11
                        $output[$mainKey][$keyDisplay] = $this->dumpNestedInternal(
191 11
                            $value,
192 11
                            $depth,
193 11
                            $level + 1,
194 11
                            $objectCollapseLevel
195
                        );
196
                    }
197
                }
198
199 13
                break;
200 13
            case 'resource':
201 1
                $output = $this->getResourceDescription($var);
202 1
                break;
203
        }
204
205 25
        return $output;
206
    }
207
208 11
    private function normalizeProperty(string $property): string
209
    {
210 11
        $property = str_replace("\0", '::', trim($property));
211
212 11
        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...
213
            return 'protected::' . substr($property, 3);
214
        }
215
216 11
        if (($pos = strpos($property, '::')) !== false) {
217
            return 'private::' . substr($property, $pos + 2);
218
        }
219
220 11
        return 'public::' . $property;
221
    }
222
223
    /**
224
     * @param mixed $var variable to be dumped
225
     * @param int $depth
226
     * @param int $level depth level
227
     * @throws \ReflectionException
228
     * @return string
229
     */
230 22
    private function dumpInternal($var, int $depth, int $level): string
231
    {
232 22
        $type = gettype($var);
233 22
        switch ($type) {
234 22
            case 'boolean':
235 1
                return $var ? 'true' : 'false';
236 21
            case 'integer':
237 19
            case 'double':
238 6
                return (string)$var;
239 19
            case 'string':
240 5
                return "'" . addslashes($var) . "'";
241 17
            case 'resource':
242 1
                return '{resource}';
243 16
            case 'NULL':
244 1
                return 'null';
245 15
            case 'unknown type':
246
                return '{unknown}';
247 15
            case 'array':
248 4
                if ($depth <= $level) {
249
                    return '[...]';
250
                }
251
252 4
                if (empty($var)) {
253 1
                    return '[]';
254
                }
255
256 3
                $output = '';
257 3
                $keys = array_keys($var);
258 3
                $spaces = str_repeat(' ', $level * 4);
259 3
                $output .= '[';
260 3
                foreach ($keys as $key) {
261 3
                    if ($this->beautify) {
262 3
                        $output .= "\n" . $spaces . '    ';
263
                    }
264 3
                    $output .= $this->dumpInternal($key, $depth, 0);
265 3
                    $output .= ' => ';
266 3
                    $output .= $this->dumpInternal($var[$key], $depth, $level + 1);
267
                }
268
269 3
                return $this->beautify
270 3
                    ? $output . "\n" . $spaces . ']'
271 3
                    : $output . ']';
272 12
            case 'object':
273 12
                if ($var instanceof \Closure) {
274 9
                    return $this->exportClosure($var);
275
                }
276 3
                if (in_array($var, self::$objects, true)) {
277
                    return $this->getObjectDescription($var) . '(...)';
278
                }
279
280 3
                if ($depth <= $level) {
281
                    return get_class($var) . '(...)';
282
                }
283
284 3
                self::$objects[] = $var;
285 3
                $spaces = str_repeat(' ', $level * 4);
286 3
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
287 3
                $dumpValues = $this->getVarDumpValuesArray($var);
288 3
                foreach ($dumpValues as $key => $value) {
289 2
                    $keyDisplay = strtr(trim($key), "\0", ':');
290 2
                    $output .= "\n" . $spaces . "    [$keyDisplay] => ";
291 2
                    $output .= $this->dumpInternal($value, $depth, $level + 1);
292
                }
293 3
                return $output . "\n" . $spaces . ')';
294
            default:
295
                return $type;
296
        }
297
    }
298
299 16
    private function getVarDumpValuesArray($var): array
300
    {
301 16
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
302 1
            $dumpValues = $var->__debugInfo();
303 1
            if (!is_array($dumpValues)) {
304
                throw new \Exception('__debugInfo() must return an array');
305
            }
306 1
            return $dumpValues;
307
        }
308
309 15
        return (array)$var;
310
    }
311
312 1
    private function getResourceDescription($resource)
313
    {
314 1
        $type = get_resource_type($resource);
315 1
        if ($type === 'stream') {
316 1
            $desc = stream_get_meta_data($resource);
317
        } else {
318
            $desc = '{resource}';
319
        }
320
321 1
        return $desc;
322
    }
323
324
    /**
325
     * @param mixed $var variable to be exported
326
     * @param int $level depth level
327
     * @return string
328
     * @throws \ReflectionException
329
     */
330 42
    private function exportInternal($var, int $level): string
331
    {
332 42
        switch (gettype($var)) {
333 42
            case 'NULL':
334 2
                return 'null';
335 40
            case 'array':
336 8
                if (empty($var)) {
337 2
                    return '[]';
338
                }
339
340 6
                $keys = array_keys($var);
341 6
                $outputKeys = ($keys !== range(0, count($var) - 1));
342 6
                $spaces = str_repeat(' ', $level * 4);
343 6
                $output = '[';
344 6
                foreach ($keys as $key) {
345 6
                    if ($this->beautify) {
346 3
                        $output .= "\n" . $spaces . '    ';
347
                    }
348 6
                    if ($outputKeys) {
349 2
                        $output .= $this->exportInternal($key, 0);
350 2
                        $output .= ' => ';
351
                    }
352 6
                    $output .= $this->exportInternal($var[$key], $level + 1);
353 6
                    if ($this->beautify || next($keys) !== false) {
354 5
                        $output .= ',';
355
                    }
356
                }
357 6
                return $this->beautify
358 3
                    ? $output . "\n" . $spaces . ']'
359 6
                    : $output . ']';
360 38
            case 'object':
361 22
                if ($var instanceof \Closure) {
362 18
                    return $this->exportClosure($var);
363
                }
364
365
                try {
366 4
                    return 'unserialize(' . var_export(serialize($var), true) . ')';
367
                } catch (\Exception $e) {
368
                    // serialize may fail, for example: if object contains a `\Closure` instance
369
                    // so we use a fallback
370
                    if ($var instanceof ArrayableInterface) {
371
                        return $this->exportInternal($var->toArray(), $level);
372
                    }
373
374
                    if ($var instanceof \IteratorAggregate) {
375
                        $varAsArray = [];
376
                        foreach ($var as $key => $value) {
377
                            $varAsArray[$key] = $value;
378
                        }
379
                        return $this->exportInternal($varAsArray, $level);
380
                    }
381
382
                    if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__toString')) {
383
                        return var_export($var->__toString(), true);
384
                    }
385
386
                    return var_export(self::create($var)->asString(), true);
387
                }
388
            default:
389 16
                return var_export($var, true);
390
        }
391
    }
392
393
    /**
394
     * Exports a [[Closure]] instance.
395
     * @param \Closure $closure closure instance.
396
     * @return string
397
     * @throws \ReflectionException
398
     */
399 37
    private function exportClosure(\Closure $closure): string
400
    {
401 37
        $reflection = new \ReflectionFunction($closure);
402
403 37
        $fileName = $reflection->getFileName();
404 37
        $start = $reflection->getStartLine();
405 37
        $end = $reflection->getEndLine();
406
407 37
        if ($fileName === false || $start === false || $end === false) {
408
            return 'function() {/* Error: unable to determine Closure source */}';
409
        }
410
411 37
        --$start;
412 37
        $uses = $this->getUsesParser()->fromFile($fileName);
413
414 37
        $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

414
        $source = implode('', array_slice(/** @scrutinizer ignore-type */ file($fileName), $start, $end - $start));
Loading history...
415 37
        $tokens = token_get_all('<?php ' . $source);
416 37
        array_shift($tokens);
417
418 37
        $closureTokens = [];
419 37
        $pendingParenthesisCount = 0;
420 37
        $isShortClosure = false;
421 37
        $buffer = '';
422 37
        foreach ($tokens as $token) {
423 37
            if (!isset($token[0])) {
424
                continue;
425
            }
426 37
            if (in_array($token[0], $this->exportClosureTokens, true)) {
427 37
                $closureTokens[] = $token[1];
428 37
                if (!$isShortClosure && $token[0] === T_FN) {
429 29
                    $isShortClosure = true;
430
                }
431 37
                continue;
432
            }
433 37
            if ($closureTokens !== []) {
434 37
                $readableToken = $token[1] ?? $token;
435 37
                if ($this->isNextTokenIsPartOfNamespace($token)) {
436 20
                    $buffer .= $token[1];
437 20
                    if (!$this->isNextTokenIsPartOfNamespace(next($tokens)) && array_key_exists($buffer, $uses)) {
438 12
                        $readableToken = $uses[$buffer];
439 12
                        $buffer = '';
440
                    }
441
                }
442 37
                if ($token === '{' || $token === '[') {
443 12
                    $pendingParenthesisCount++;
444 37
                } elseif ($token === '}' || $token === ']') {
445 16
                    if ($pendingParenthesisCount === 0) {
446 4
                        break;
447
                    }
448 12
                    $pendingParenthesisCount--;
449 37
                } elseif ($token === ',' || $token === ';') {
450 33
                    if ($pendingParenthesisCount === 0) {
451 33
                        break;
452
                    }
453
                }
454 37
                $closureTokens[] = $readableToken;
455
            }
456
        }
457
458 37
        return implode('', $closureTokens);
459
    }
460
461 19
    public function asPhpString(): string
462
    {
463 19
        $this->exportClosureTokens = [T_FUNCTION, T_FN, T_STATIC];
464 19
        $this->beautify = false;
465 19
        return $this->export();
466
    }
467
468 37
    private function getUsesParser(): UseStatementParser
469
    {
470 37
        if ($this->useStatementParser === null) {
471 37
            $this->useStatementParser = new UseStatementParser();
472
        }
473
474 37
        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...
475
    }
476
477 37
    private function isNextTokenIsPartOfNamespace($token): bool
478
    {
479 37
        if (!is_array($token)) {
480 37
            return false;
481
        }
482
483 37
        return $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR;
484
    }
485
486 15
    private function getObjectDescription(object $object): string
487
    {
488 15
        return get_class($object) . '#' . spl_object_id($object);
489
    }
490
}
491