Passed
Pull Request — master (#30)
by Dmitriy
02:14
created

VarDumper::dumpNestedInternal()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 60
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 12.0654

Importance

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

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