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

VarDumper::getObjectsMap()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 11
ccs 7
cts 8
cp 0.875
crap 3.0175
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 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