Passed
Pull Request — master (#22)
by Dmitriy
02:04
created

VarDumper::getResourceDescription()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
crap 2.0185
rs 10
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 71
    private function __construct($variable)
31
    {
32 71
        $this->variable = $variable;
33 71
    }
34
35 71
    public static function create($variable): self
36
    {
37 71
        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 7
    private function asArray(int $depth, int $objectCollapseLevel = 0): array
74
    {
75 7
        $this->buildVarObjectsCache($this->variable, $depth);
76 7
        return $this->dumpNestedInternal($this->variable, $depth, 0, $objectCollapseLevel);
77
    }
78
79 5
    public function asJson(int $depth = 50, bool $prettyPrint = false): string
80
    {
81 5
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
82
83 5
        if ($prettyPrint) {
84
            $options |= JSON_PRETTY_PRINT;
85
        }
86
87 5
        return json_encode($this->asArray($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->getObjectsMap($this->asArray($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 7
    private function buildVarObjectsCache($var, int $depth, int $level = 0): void
128
    {
129 7
        if (is_array($var)) {
130 4
            if ($depth <= $level) {
131
                return;
132
            }
133 4
            foreach ($var as $key => $value) {
134 4
                $this->buildVarObjectsCache($value, $depth, $level + 1);
135
            }
136 7
        } elseif (is_object($var)) {
137 4
            if ($depth <= $level || in_array($var, self::$objects, true)) {
138 3
                return;
139
            }
140 4
            self::$objects[] = $var;
141 4
            $dumpValues = $this->getVarDumpValuesArray($var);
142 4
            foreach ($dumpValues as $key => $value) {
143 4
                $this->buildVarObjectsCache($value, $depth, $level + 1);
144
            }
145
        }
146 7
    }
147
148 7
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
149
    {
150 7
        $output = $var;
151
152 7
        switch (gettype($var)) {
153 7
            case 'array':
154 4
                if ($depth <= $level) {
155
                    return 'array [...]';
156
                }
157
158 4
                $output = [];
159 4
                foreach ($var as $key => $value) {
160 4
                    if (is_object($value)) {
161 2
                        $keyDisplay = spl_object_id($value);
162
                    } else {
163 2
                        $keyDisplay = str_replace("\0", '::', trim($key));
164
                    }
165 4
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
166
                }
167
168 4
                break;
169 7
            case 'object':
170 4
                $className = get_class($var);
171
                /**
172
                 * @psalm-var array<string, array<string, array|string>> $output
173
                 */
174 4
                if (($objectCollapseLevel < $level) && (in_array($var, self::$objects, true))) {
175 2
                    if ($var instanceof \Closure) {
176 1
                        $output = $this->exportClosure($var);
177
                    } else {
178 2
                        $output = 'object@' . $this->getObjectDescription($var);
179
                    }
180 4
                } elseif ($depth <= $level) {
181
                    $output = $className . ' (...)';
182
                } else {
183 4
                    $output = [];
184 4
                    $dumpValues = $this->getVarDumpValuesArray($var);
185 4
                    if (empty($dumpValues)) {
186
                        $output[$className] = '{stateless object}';
187
                    }
188 4
                    foreach ($dumpValues as $key => $value) {
189 4
                        $keyDisplay = $this->normalizeProperty($key);
190
                        /**
191
                         * @psalm-suppress InvalidArrayOffset
192
                         */
193 4
                        $output[$className][$keyDisplay] = $this->dumpNestedInternal(
194 4
                            $value,
195 4
                            $depth,
196 4
                            $level + 1,
197 4
                            $objectCollapseLevel
198
                        );
199
                    }
200
                }
201
202 4
                break;
203 6
            case 'resource':
204 1
                $output = $this->getResourceDescription($var);
205 1
                break;
206
        }
207
208 7
        return $output;
209
    }
210
211 4
    private function normalizeProperty(string $property): string
212
    {
213 4
        $property = str_replace("\0", '::', trim($property));
214
215 4
        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 4
        if (($pos = strpos($property, '::')) !== false) {
220
            return 'private::' . substr($property, $pos + 2);
221
        }
222
223 4
        return 'public::' . $property;
224
    }
225
226 2
    private function getObjectsMap(array $objectsArray): array
227
    {
228 2
        $objects = [];
229 2
        foreach ($objectsArray as $index => $object) {
230 2
            if (!is_array($object)) {
231
                continue;
232
            }
233 2
            $className = array_key_first($object);
234 2
            $objects[$className . '#' . $index] = $object[$className];
235
        }
236 2
        return $objects;
237
    }
238
239
    /**
240
     * @param mixed $var variable to be dumped
241
     * @param int $level depth level
242
     * @return string
243
     */
244 22
    private function dumpInternal($var, int $depth, int $level): string
245
    {
246 22
        $type = gettype($var);
247 22
        switch ($type) {
248 22
            case 'boolean':
249 1
                return $var ? 'true' : 'false';
250 21
            case 'integer':
251 19
            case 'double':
252 6
                return (string)$var;
253 19
            case 'string':
254 5
                return "'" . addslashes($var) . "'";
255 17
            case 'resource':
256 1
                return '{resource}';
257 16
            case 'NULL':
258 1
                return 'null';
259 15
            case 'unknown type':
260
                return '{unknown}';
261 15
            case 'array':
262 4
                if ($depth <= $level) {
263
                    return '[...]';
264
                }
265
266 4
                if (empty($var)) {
267 1
                    return '[]';
268
                }
269
270 3
                $output = '';
271 3
                $keys = array_keys($var);
272 3
                $spaces = str_repeat(' ', $level * 4);
273 3
                $output .= '[';
274 3
                foreach ($keys as $key) {
275 3
                    if ($this->beautify) {
276 3
                        $output .= "\n" . $spaces . '    ';
277
                    }
278 3
                    $output .= $this->dumpInternal($key, $depth, 0);
279 3
                    $output .= ' => ';
280 3
                    $output .= $this->dumpInternal($var[$key], $depth, $level + 1);
281
                }
282
283 3
                return $this->beautify
284 3
                    ? $output . "\n" . $spaces . ']'
285 3
                    : $output . ']';
286 12
            case 'object':
287 12
                if ($var instanceof \Closure) {
288 9
                    return $this->exportClosure($var);
289
                }
290 3
                if (in_array($var, self::$objects, true)) {
291
                    return $this->getObjectDescription($var) . '(...)';
292
                }
293
294 3
                if ($depth <= $level) {
295
                    return get_class($var) . '(...)';
296
                }
297
298 3
                self::$objects[] = $var;
299 3
                $spaces = str_repeat(' ', $level * 4);
300 3
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
301 3
                $dumpValues = $this->getVarDumpValuesArray($var);
302 3
                foreach ($dumpValues as $key => $value) {
303 2
                    $keyDisplay = strtr(trim($key), "\0", ':');
304 2
                    $output .= "\n" . $spaces . "    [$keyDisplay] => ";
305 2
                    $output .= $this->dumpInternal($value, $depth, $level + 1);
306
                }
307 3
                return $output . "\n" . $spaces . ')';
308
            default:
309
                return $type;
310
        }
311
    }
312
313 7
    private function getVarDumpValuesArray($var): array
314
    {
315 7
        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 6
        return (array)$var;
324
    }
325
326 1
    private function getResourceDescription($resource)
327
    {
328 1
        $type = get_resource_type($resource);
329 1
        if ($type === 'stream') {
330 1
            $desc = stream_get_meta_data($resource);
331
        } else {
332
            $desc = '{resource}';
333
        }
334
335 1
        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 42
    private function exportInternal($var, int $level): string
345
    {
346 42
        switch (gettype($var)) {
347 42
            case 'NULL':
348 2
                return 'null';
349 40
            case 'array':
350 8
                if (empty($var)) {
351 2
                    return '[]';
352
                }
353
354 6
                $keys = array_keys($var);
355 6
                $outputKeys = ($keys !== range(0, count($var) - 1));
356 6
                $spaces = str_repeat(' ', $level * 4);
357 6
                $output = '[';
358 6
                foreach ($keys as $key) {
359 6
                    if ($this->beautify) {
360 3
                        $output .= "\n" . $spaces . '    ';
361
                    }
362 6
                    if ($outputKeys) {
363 2
                        $output .= $this->exportInternal($key, 0);
364 2
                        $output .= ' => ';
365
                    }
366 6
                    $output .= $this->exportInternal($var[$key], $level + 1);
367 6
                    if ($this->beautify || next($keys) !== false) {
368 5
                        $output .= ',';
369
                    }
370
                }
371 6
                return $this->beautify
372 3
                    ? $output . "\n" . $spaces . ']'
373 6
                    : $output . ']';
374 38
            case 'object':
375 22
                if ($var instanceof \Closure) {
376 18
                    return $this->exportClosure($var);
377
                }
378
379
                try {
380 4
                    return 'unserialize(' . var_export(serialize($var), true) . ')';
381
                } catch (\Exception $e) {
382
                    // serialize may fail, for example: if object contains a `\Closure` instance
383
                    // so we use a fallback
384
                    if ($var instanceof ArrayableInterface) {
385
                        return $this->exportInternal($var->toArray(), $level);
386
                    }
387
388
                    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
                    if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__toString')) {
397
                        return var_export($var->__toString(), true);
398
                    }
399
400
                    return var_export(self::create($var)->asString(), true);
401
                }
402
            default:
403 16
                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 28
    private function exportClosure(\Closure $closure): string
414
    {
415 28
        $reflection = new \ReflectionFunction($closure);
416
417 28
        $fileName = $reflection->getFileName();
418 28
        $start = $reflection->getStartLine();
419 28
        $end = $reflection->getEndLine();
420
421 28
        if ($fileName === false || $start === false || $end === false) {
422
            return 'function() {/* Error: unable to determine Closure source */}';
423
        }
424
425 28
        --$start;
426 28
        $uses = $this->getUsesParser()->fromFile($fileName);
427
428 28
        $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 28
        $tokens = token_get_all('<?php ' . $source);
430 28
        array_shift($tokens);
431
432 28
        $closureTokens = [];
433 28
        $pendingParenthesisCount = 0;
434 28
        $isShortClosure = false;
435 28
        $buffer = '';
436 28
        foreach ($tokens as $token) {
437 28
            if (!isset($token[0])) {
438
                continue;
439
            }
440 28
            if (in_array($token[0], $this->exportClosureTokens, true)) {
441 28
                $closureTokens[] = $token[1];
442 28
                if (!$isShortClosure && $token[0] === T_FN) {
443 22
                    $isShortClosure = true;
444
                }
445 28
                continue;
446
            }
447 28
            if ($closureTokens !== []) {
448 28
                $readableToken = $token[1] ?? $token;
449 28
                if ($this->isNextTokenIsPartOfNamespace($token)) {
450 15
                    $buffer .= $token[1];
451 15
                    if (!$this->isNextTokenIsPartOfNamespace(next($tokens)) && array_key_exists($buffer, $uses)) {
452 9
                        $readableToken = $uses[$buffer];
453 9
                        $buffer = '';
454
                    }
455
                }
456 28
                if ($token === '{' || $token === '[') {
457 9
                    $pendingParenthesisCount++;
458 28
                } elseif ($token === '}' || $token === ']') {
459 12
                    if ($pendingParenthesisCount === 0) {
460 3
                        break;
461
                    }
462 9
                    $pendingParenthesisCount--;
463 28
                } elseif ($token === ',' || $token === ';') {
464 25
                    if ($pendingParenthesisCount === 0) {
465 25
                        break;
466
                    }
467
                }
468 28
                $closureTokens[] = $readableToken;
469
            }
470
        }
471
472 28
        return implode('', $closureTokens);
473
    }
474
475 19
    public function asPhpString(): string
476
    {
477 19
        $this->exportClosureTokens = [T_FUNCTION, T_FN, T_STATIC];
478 19
        $this->beautify = false;
479 19
        return $this->export();
480
    }
481
482 28
    private function getUsesParser(): UseStatementParser
483
    {
484 28
        if ($this->useStatementParser === null) {
485 28
            $this->useStatementParser = new UseStatementParser();
486
        }
487
488 28
        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 28
    private function isNextTokenIsPartOfNamespace($token): bool
492
    {
493 28
        if (!is_array($token)) {
494 28
            return false;
495
        }
496
497 28
        return $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR;
498
    }
499
500 4
    private function getObjectDescription(object $object): string
501
    {
502 4
        return get_class($object) . '#' . spl_object_id($object);
503
    }
504
}
505