Passed
Pull Request — master (#75)
by Dmitriy
02:20
created

VarDumper::asPrimitives()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use __PHP_Incomplete_Class;
8
use Closure;
9
use Exception;
10
use IteratorAggregate;
11
use JsonException;
12
use JsonSerializable;
13
use ReflectionObject;
14
use ReflectionException;
15
use Yiisoft\Arrays\ArrayableInterface;
16
17
use function array_keys;
18
use function get_class;
19
use function gettype;
20
use function highlight_string;
21
use function method_exists;
22
use function next;
23
use function preg_replace;
24
use function spl_object_id;
25
use function str_repeat;
26
use function strtr;
27
use function trim;
28
use function var_export;
29
30
/**
31
 * VarDumper provides enhanced versions of the PHP functions {@see var_dump()} and {@see var_export()}.
32
 * It can:
33
 *
34
 * - Correctly identify the recursively referenced objects in a complex object structure.
35
 * - Recursively control depth to avoid indefinite recursive display of some peculiar variables.
36
 * - Export closures and objects.
37
 * - Highlight output.
38
 * - Format output.
39
 */
40
final class VarDumper
41
{
42
    public const OBJECT_ID_PROPERTY = '$__id__$';
43
    public const OBJECT_CLASS_PROPERTY = '$__class__$';
44
    public const DEPTH_LIMIT_EXCEEDED_PROPERTY = '$__depth_limit_exceeded__$';
45
    /**
46
     * @var mixed Variable to dump.
47
     */
48
    private $variable;
49
    /**
50
     * @var string[] Variables using in closure scope.
51
     */
52
    private array $useVarInClosures = [];
53
    private bool $serializeObjects = true;
54
    /**
55
     * @var string Offset to use to indicate nesting level.
56
     */
57
    private string $offset = '    ';
58
    private static ?ClosureExporter $closureExporter = null;
59
60
    /**
61
     * @param mixed $variable Variable to dump.
62
     */
63 139
    private function __construct($variable)
64
    {
65 139
        $this->variable = $variable;
66
    }
67
68
    /**
69
     * @param mixed $variable Variable to dump.
70
     *
71
     * @return static An instance containing variable to dump.
72
     */
73 139
    public static function create($variable): self
74
    {
75 139
        return new self($variable);
76
    }
77
78
    /**
79
     * Prints a variable.
80
     *
81
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
82
     * but is more robust when handling complex objects.
83
     *
84
     * @param mixed $variable Variable to be dumped.
85
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
86
     * @param bool $highlight Whether the result should be syntax-highlighted.
87
     *
88
     * @throws ReflectionException
89
     */
90 6
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
91
    {
92 6
        echo self::create($variable)->asString($depth, $highlight);
93
    }
94
95
    /**
96
     * Sets offset string to use to indicate nesting level.
97
     *
98
     * @param string $offset The offset string.
99
     *
100
     * @return static New instance with a given offset.
101
     */
102
    public function withOffset(string $offset): self
103
    {
104
        $new = clone $this;
105
        $new->offset = $offset;
106
107
        return $new;
108
    }
109
110
    /**
111
     * Dumps a variable in terms of a string.
112
     *
113
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
114
     * but is more robust when handling complex objects.
115
     *
116
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
117
     * @param bool $highlight Whether the result should be syntax-highlighted.
118
     *
119
     * @throws ReflectionException
120
     *
121
     * @return string The string representation of the variable.
122
     */
123 32
    public function asString(int $depth = 10, bool $highlight = false): string
124
    {
125 32
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
126
127 32
        if ($highlight) {
128 1
            $result = highlight_string("<?php\n" . $output, true);
129 1
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
130
        }
131
132 32
        return $output;
133
    }
134
135
    /**
136
     * Dumps a variable as a JSON string.
137
     *
138
     * This method provides similar functionality to the {@see json_encode()}
139
     * but is more robust when handling complex objects.
140
     *
141
     * @param bool $format Use whitespaces in returned data to format it
142
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
143
     *
144
     * @throws JsonException
145
     *
146
     * @return string The json representation of the variable.
147
     */
148 28
    public function asJson(bool $format = true, int $depth = 10): string
149
    {
150 28
        $output = $this->asPrimitives($depth);
151
152 28
        if ($format) {
153 28
            return json_encode($output, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
154
        }
155
156
        return json_encode($output, JSON_THROW_ON_ERROR);
157
    }
158
159
    /**
160
     * Dumps the variable as PHP primitives in JSON decoded style.
161
     *
162
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
163
     *
164
     * @throws ReflectionException
165
     *
166
     * @return mixed
167
     */
168 56
    public function asPrimitives(int $depth = 10): mixed
169
    {
170 56
        return $this->exportPrimitives($this->variable, $depth, 0);
171
    }
172
173
    /**
174
     * Exports a variable as a string containing PHP code.
175
     *
176
     * The string is a valid PHP expression that can be evaluated by PHP parser
177
     * and the evaluation result will give back the variable value.
178
     *
179
     * This method is similar to {@see var_export()}. The main difference is that
180
     * it generates more compact string representation using short array syntax.
181
     *
182
     * It also handles closures with {@see ClosureExporter} and objects
183
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
184
     *
185
     * @param bool $format Whatever to format code.
186
     * @param string[] $useVariables Array of variables used in `use` statement (['$params', '$config'])
187
     * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it
188
     * is false only objects of internal classes will be serialized.
189
     *
190
     * @throws ReflectionException
191
     *
192
     * @return string A PHP code representation of the variable.
193
     */
194 53
    public function export(bool $format = true, array $useVariables = [], bool $serializeObjects = true): string
195
    {
196 53
        $this->useVarInClosures = $useVariables;
197 53
        $this->serializeObjects = $serializeObjects;
198 53
        return $this->exportInternal($this->variable, $format, 0);
199
    }
200
201
    /**
202
     * @param mixed $var Variable to be dumped.
203
     * @param bool $format Whatever to format code.
204
     * @param int $depth Maximum depth.
205
     * @param int $level Current depth.
206
     *
207
     * @throws ReflectionException
208
     *
209
     * @return string
210
     */
211 32
    private function dumpInternal($var, bool $format, int $depth, int $level): string
212
    {
213 32
        switch (gettype($var)) {
214 32
            case 'resource':
215 31
            case 'resource (closed)':
216 1
                return '{resource}';
217 31
            case 'NULL':
218 1
                return 'null';
219 30
            case 'array':
220 6
                if ($depth <= $level) {
221 1
                    return '[...]';
222
                }
223
224 5
                if (empty($var)) {
225 2
                    return '[]';
226
                }
227
228 3
                $output = '';
229 3
                $keys = array_keys($var);
230 3
                $spaces = str_repeat($this->offset, $level);
231 3
                $output .= '[';
232
233 3
                foreach ($keys as $name) {
234 3
                    if ($format) {
235 3
                        $output .= "\n" . $spaces . $this->offset;
236
                    }
237 3
                    $output .= $this->exportVariable($name);
238 3
                    $output .= ' => ';
239 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
240
                }
241
242 3
                return $format
243 3
                    ? $output . "\n" . $spaces . ']'
244 3
                    : $output . ']';
245 28
            case 'object':
246 16
                if ($var instanceof Closure) {
247 11
                    return $this->exportClosure($var);
248
                }
249
250 7
                if ($depth <= $level) {
251 1
                    return $this->getObjectDescription($var) . ' (...)';
252
                }
253
254 6
                $spaces = str_repeat($this->offset, $level);
255 6
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
256 6
                $objectProperties = $this->getObjectProperties($var);
257
258
                /** @psalm-var mixed $value */
259 6
                foreach ($objectProperties as $name => $value) {
260 4
                    $propertyName = strtr(trim((string) $name), "\0", '::');
261 4
                    $output .= "\n" . $spaces . $this->offset . '[' . $propertyName . '] => ';
262 4
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
263
                }
264 6
                return $output . "\n" . $spaces . ')';
265
            default:
266 14
                return $this->exportVariable($var);
267
        }
268
    }
269
270
    /**
271
     * @param mixed $variable Variable to be exported.
272
     * @param bool $format Whatever to format code.
273
     * @param int $level Current depth.
274
     *
275
     * @throws ReflectionException
276
     *
277
     * @return string
278
     */
279 53
    private function exportInternal($variable, bool $format, int $level): string
280
    {
281 53
        $spaces = str_repeat($this->offset, $level);
282 53
        switch (gettype($variable)) {
283 53
            case 'NULL':
284 2
                return 'null';
285 51
            case 'array':
286 9
                if (empty($variable)) {
287 2
                    return '[]';
288
                }
289
290 7
                $keys = array_keys($variable);
291 7
                $outputKeys = $keys !== array_keys($keys);
292 7
                $output = '[';
293
294 7
                foreach ($keys as $key) {
295 7
                    if ($format) {
296 4
                        $output .= "\n" . $spaces . $this->offset;
297
                    }
298 7
                    if ($outputKeys) {
299 3
                        $output .= $this->exportVariable($key);
300 3
                        $output .= ' => ';
301
                    }
302 7
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
303 7
                    if ($format || next($keys) !== false) {
304 6
                        $output .= ',';
305
                    }
306
                }
307
308 7
                return $format
309 4
                    ? $output . "\n" . $spaces . ']'
310 7
                    : $output . ']';
311 49
            case 'object':
312 32
                if ($variable instanceof Closure) {
313 25
                    return $this->exportClosure($variable, $level);
314
                }
315
316 12
                $reflectionObject = new ReflectionObject($variable);
317
                try {
318 12
                    if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) {
319 10
                        return "unserialize({$this->exportVariable(serialize($variable))})";
320
                    }
321
322 2
                    return $this->exportObject($variable, $reflectionObject, $format, $level);
323 6
                } catch (Exception $e) {
324
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
325 6
                    if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) {
326
                        try {
327 4
                            return $this->exportObject($variable, $reflectionObject, $format, $level);
328
                        } catch (Exception $e) {
329
                            return $this->exportObjectFallback($variable, $format, $level);
330
                        }
331
                    }
332
333 2
                    return $this->exportObjectFallback($variable, $format, $level);
334
                }
335
            default:
336 19
                return $this->exportVariable($variable);
337
        }
338
    }
339
340
    /**
341
     * @param mixed $var
342
     * @param int $depth
343
     * @param int $level
344
     *
345
     * @throws ReflectionException
346
     *
347
     * @return mixed
348
     * @psalm-param mixed $var
349
     */
350 56
    private function exportPrimitives(mixed $var, int $depth, int $level): mixed
351
    {
352 56
        switch (gettype($var)) {
353 56
            case 'resource':
354 54
            case 'resource (closed)':
355 2
                return '{resource}';
356 54
            case 'array':
357 12
                if ($depth <= $level) {
358
                    return [
359 2
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
360
                    ];
361
                }
362
363
                /** @psalm-suppress MissingClosureReturnType */
364 12
                return array_map(fn ($value) => $this->exportPrimitives($value, $depth, $level + 1), $var);
365 50
            case 'object':
366 32
                if ($var instanceof Closure) {
367 20
                    return $this->exportClosure($var);
368
                }
369
370 14
                $objectClass = get_class($var);
371 14
                $objectId = $this->getObjectId($var);
372 14
                if ($depth <= $level) {
373
                    return [
374 2
                        self::OBJECT_ID_PROPERTY => $objectId,
375
                        self::OBJECT_CLASS_PROPERTY => $objectClass,
376
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
377
                    ];
378
                }
379
380 14
                $objectProperties = $this->getObjectProperties($var);
381
382 14
                $output = [
383
                    self::OBJECT_ID_PROPERTY => $objectId,
384
                    self::OBJECT_CLASS_PROPERTY => $objectClass,
385
                ];
386
                /**
387
                 * @psalm-var mixed $value
388
                 * @psalm-var string $name
389
                 */
390 14
                foreach ($objectProperties as $name => $value) {
391 12
                    $propertyName = $this->getPropertyName($name);
392
                    /** @psalm-suppress MixedAssignment */
393 12
                    $output[$propertyName] = $this->exportPrimitives($value, $depth, $level + 1);
394
                }
395 14
                return $output;
396
            default:
397 26
                return $var;
398
        }
399
    }
400
401 18
    private function getPropertyName(string|int $property): string
402
    {
403 18
        if (is_int($property)) {
404 2
            return (string) $property;
405
        }
406 16
        $property = str_replace("\0", '::', trim($property));
407
408 16
        if (str_starts_with($property, '*::')) {
409
            return substr($property, 3);
410
        }
411
412 16
        if (($pos = strpos($property, '::')) !== false) {
413 6
            return substr($property, $pos + 2);
414
        }
415
416 10
        return $property;
417
    }
418
419
    /**
420
     * @param object $variable
421
     * @param bool $format
422
     * @param int $level
423
     *
424
     * @throws ReflectionException
425
     *
426
     * @return string
427
     */
428 2
    private function exportObjectFallback(object $variable, bool $format, int $level): string
429
    {
430 2
        if ($variable instanceof ArrayableInterface) {
431
            return $this->exportInternal($variable->toArray(), $format, $level);
432
        }
433
434 2
        if ($variable instanceof JsonSerializable) {
435
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
436
        }
437
438 2
        if ($variable instanceof IteratorAggregate) {
439
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
440
        }
441
442
        /** @psalm-suppress RedundantCondition */
443 2
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
444
            return $this->exportVariable($variable->__toString());
445
        }
446
447 2
        return $this->exportVariable(self::create($variable)->asString());
448
    }
449
450 6
    private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string
451
    {
452 6
        $spaces = str_repeat($this->offset, $level);
453 6
        $objectProperties = $this->getObjectProperties($variable);
454 6
        $class = get_class($variable);
455 6
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(', ', $this->useVarInClosures) . ')';
456 6
        $lines = ['(static function ()' . $use . ' {',];
457 6
        if ($reflectionObject->getConstructor() === null) {
458 2
            $lines = array_merge($lines, [
459 2
                $this->offset . '$object = new ' . $class . '();',
460 2
                $this->offset . '(function ()' . $use . ' {',
461
            ]);
462
        } else {
463 4
            $lines = array_merge($lines, [
464 4
                $this->offset . '$class = new \ReflectionClass(\'' . $class . '\');',
465 4
                $this->offset . '$object = $class->newInstanceWithoutConstructor();',
466 4
                $this->offset . '(function ()' . $use . ' {',
467
            ]);
468
        }
469 6
        $endLines = [
470 6
            $this->offset . '})->bindTo($object, \'' . $class . '\')();',
471
            '',
472 6
            $this->offset . 'return $object;',
473
            '})()',
474
        ];
475
476
        /**
477
         * @psalm-var mixed $value
478
         * @psalm-var string $name
479
         */
480 6
        foreach ($objectProperties as $name => $value) {
481 6
            $propertyName = $this->getPropertyName($name);
482 6
            $lines[] = $this->offset . $this->offset . '$this->' . $propertyName . ' = ' .
483 6
                $this->exportInternal($value, $format, $level + 2) . ';';
484
        }
485
486 6
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
487
    }
488
489
    /**
490
     * Exports a {@see \Closure} instance.
491
     *
492
     * @param Closure $closure Closure instance.
493
     *
494
     * @throws ReflectionException
495
     *
496
     * @return string
497
     */
498 56
    private function exportClosure(Closure $closure, int $level = 0): string
499
    {
500 56
        if (self::$closureExporter === null) {
501 1
            self::$closureExporter = new ClosureExporter();
502
        }
503
504 56
        return self::$closureExporter->export($closure, $level);
0 ignored issues
show
Bug introduced by
The method export() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

504
        return self::$closureExporter->/** @scrutinizer ignore-call */ export($closure, $level);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
505
    }
506
507
    /**
508
     * @param mixed $variable
509
     *
510
     * @return string
511
     */
512 41
    private function exportVariable($variable): string
513
    {
514 41
        return var_export($variable, true);
515
    }
516
517 7
    private function getObjectDescription(object $object): string
518
    {
519 7
        return get_class($object) . '#' . $this->getObjectId($object);
520
    }
521
522 21
    private function getObjectId(object $object): string
523
    {
524 21
        return (string) spl_object_id($object);
525
    }
526
527 26
    private function getObjectProperties(object $var): array
528
    {
529 26
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
530
            /** @var array $var */
531 3
            $var = $var->__debugInfo();
532
        }
533
534 26
        return (array) $var;
535
    }
536
}
537