Passed
Push — master ( b2a7ab...efdb4a )
by Alexander
02:21
created

VarDumper::dumpNestedInternal()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 58
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 12.003

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

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