Test Failed
Push — master ( 427759...b5d2db )
by
unknown
02:24
created

VarDumper   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 484
Duplicated Lines 0 %

Test Coverage

Coverage 92.12%

Importance

Changes 17
Bugs 2 Features 0
Metric Value
eloc 185
c 17
b 2
f 0
dl 0
loc 484
ccs 152
cts 165
cp 0.9212
rs 2.32
wmc 76

19 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 3 1
A withOffset() 0 6 1
A __construct() 0 3 1
D exportInternal() 0 58 20
A asJson() 0 10 2
A dump() 0 3 1
A asString() 0 10 2
C dumpInternal() 0 56 14
A export() 0 5 1
A getObjectId() 0 3 1
A getObjectClass() 0 3 1
A exportVariable() 0 3 1
A getObjectProperties() 0 8 3
A exportObject() 0 37 5
A exportObjectFallback() 0 20 6
A exportClosure() 0 7 2
A getObjectDescription() 0 3 1
A getPropertyName() 0 13 3
B exportJson() 0 47 10

How to fix   Complexity   

Complex Class

Complex classes like VarDumper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use VarDumper, and based on these observations, apply Extract Interface, too.

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 107
    /**
61
     * @param mixed $variable Variable to dump.
62 107
     */
63
    private function __construct($variable)
64
    {
65
        $this->variable = $variable;
66
    }
67
68
    /**
69
     * @param mixed $variable Variable to dump.
70 107
     *
71
     * @return static An instance containing variable to dump.
72 107
     */
73
    public static function create($variable): self
74
    {
75
        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 6
     *
88
     * @throws ReflectionException
89 6
     */
90
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
91
    {
92
        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 32
     *
121
     * @return string The string representation of the variable.
122 32
     */
123
    public function asString(int $depth = 10, bool $highlight = false): string
124 32
    {
125 1
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
126 1
127
        if ($highlight) {
128
            $result = highlight_string("<?php\n" . $output, true);
129 32
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
130
        }
131
132
        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 24
     *
146
     * @return string The json representation of the variable.
147
     */
148 24
    public function asJson(bool $format = true, int $depth = 10): string
149
    {
150 24
        /** @var mixed $output */
151 24
        $output = $this->exportJson($this->variable, $format, $depth, 0);
152
153
        if ($format) {
154
            return json_encode($output, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
155
        }
156
157
        return json_encode($output, JSON_THROW_ON_ERROR);
158
    }
159
160
    /**
161
     * Exports a variable as a string containing PHP code.
162
     *
163
     * The string is a valid PHP expression that can be evaluated by PHP parser
164
     * and the evaluation result will give back the variable value.
165
     *
166
     * This method is similar to {@see var_export()}. The main difference is that
167
     * it generates more compact string representation using short array syntax.
168
     *
169
     * It also handles closures with {@see ClosureExporter} and objects
170
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
171
     *
172
     * @param bool $format Whatever to format code.
173
     * @param string[] $useVariables Array of variables used in `use` statement (['$params', '$config'])
174
     * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it
175
     * is false only objects of internal classes will be serialized.
176
     *
177
     * @throws ReflectionException
178 53
     *
179
     * @return string A PHP code representation of the variable.
180 53
     */
181 53
    public function export(bool $format = true, array $useVariables = [], bool $serializeObjects = true): string
182 53
    {
183
        $this->useVarInClosures = $useVariables;
184
        $this->serializeObjects = $serializeObjects;
185
        return $this->exportInternal($this->variable, $format, 0);
186
    }
187
188
    /**
189
     * @param mixed $var Variable to be dumped.
190
     * @param bool $format Whatever to format code.
191
     * @param int $depth Maximum depth.
192
     * @param int $level Current depth.
193
     *
194
     * @throws ReflectionException
195 45
     *
196
     * @return string
197 45
     */
198 45
    private function dumpInternal($var, bool $format, int $depth, int $level): string
199 44
    {
200 1
        switch (gettype($var)) {
201 44
            case 'resource':
202 1
            case 'resource (closed)':
203 43
                return '{resource}';
204 6
            case 'NULL':
205 1
                return 'null';
206
            case 'array':
207
                if ($depth <= $level) {
208 5
                    return '[...]';
209 2
                }
210
211
                if (empty($var)) {
212 3
                    return '[]';
213 3
                }
214 3
215 3
                $output = '';
216
                $keys = array_keys($var);
217 3
                $spaces = str_repeat($this->offset, $level);
218 3
                $output .= '[';
219 3
220
                foreach ($keys as $name) {
221 3
                    if ($format) {
222 3
                        $output .= "\n" . $spaces . $this->offset;
223 3
                    }
224
                    $output .= $this->exportVariable($name);
225
                    $output .= ' => ';
226 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
227 3
                }
228 3
229 41
                return $format
230 29
                    ? $output . "\n" . $spaces . ']'
231 21
                    : $output . ']';
232
            case 'object':
233
                if ($var instanceof Closure) {
234 11
                    return $this->exportClosure($var);
235 1
                }
236
237
                if ($depth <= $level) {
238 10
                    return $this->getObjectDescription($var) . ' (...)';
239 10
                }
240 10
241
                $spaces = str_repeat($this->offset, $level);
242
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
243 10
                $objectProperties = $this->getObjectProperties($var);
244 7
245 7
                /** @psalm-var mixed $value */
246 7
                foreach ($objectProperties as $name => $value) {
247
                    $propertyName = strtr(trim((string) $name), "\0", '::');
248 10
                    $output .= "\n" . $spaces . $this->offset . '[' . $propertyName . '] => ';
249
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
250 16
                }
251
                return $output . "\n" . $spaces . ')';
252
            default:
253
                return $this->exportVariable($var);
254
        }
255
    }
256
257
    /**
258
     * @param mixed $variable Variable to be exported.
259
     * @param bool $format Whatever to format code.
260
     * @param int $level Current depth.
261
     *
262
     * @throws ReflectionException
263 53
     *
264
     * @return string
265 53
     */
266 53
    private function exportInternal($variable, bool $format, int $level): string
267 53
    {
268 2
        $spaces = str_repeat($this->offset, $level);
269 51
        switch (gettype($variable)) {
270 9
            case 'NULL':
271 2
                return 'null';
272
            case 'array':
273
                if (empty($variable)) {
274 7
                    return '[]';
275 7
                }
276 7
277
                $keys = array_keys($variable);
278 7
                $outputKeys = $keys !== array_keys($keys);
279 7
                $output = '[';
280 4
281
                foreach ($keys as $key) {
282 7
                    if ($format) {
283 3
                        $output .= "\n" . $spaces . $this->offset;
284 3
                    }
285
                    if ($outputKeys) {
286 7
                        $output .= $this->exportVariable($key);
287 7
                        $output .= ' => ';
288 6
                    }
289
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
290
                    if ($format || next($keys) !== false) {
291
                        $output .= ',';
292 7
                    }
293 4
                }
294 7
295 49
                return $format
296 32
                    ? $output . "\n" . $spaces . ']'
297 25
                    : $output . ']';
298
            case 'object':
299
                if ($variable instanceof Closure) {
300 12
                    return $this->exportClosure($variable, $level);
301
                }
302 12
303 10
                $reflectionObject = new ReflectionObject($variable);
304
                try {
305
                    if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) {
306 2
                        return "unserialize({$this->exportVariable(serialize($variable))})";
307 6
                    }
308
309 6
                    return $this->exportObject($variable, $reflectionObject, $format, $level);
310
                } catch (Exception $e) {
311 4
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
312
                    if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) {
313
                        try {
314
                            return $this->exportObject($variable, $reflectionObject, $format, $level);
315
                        } catch (Exception $e) {
316
                            return $this->exportObjectFallback($variable, $format, $level);
317 2
                        }
318
                    }
319
320 19
                    return $this->exportObjectFallback($variable, $format, $level);
321
                }
322
            default:
323
                return $this->exportVariable($variable);
324
        }
325
    }
326
327
    /**
328
     * @param mixed $var
329
     * @param bool $format
330
     * @param int $depth
331
     * @param int $level
332
     *
333
     * @throws ReflectionException
334
     *
335 24
     * @return mixed
336
     * @psalm-param mixed $var
337 24
     */
338 24
    private function exportJson($var, bool $format, int $depth, int $level)
339 23
    {
340 1
        switch (gettype($var)) {
341 23
            case 'resource':
342 4
            case 'resource (closed)':
343
                return '{resource}';
344
            case 'array':
345
                if ($depth <= $level) {
346
                    return [
347 4
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
348 3
                    ];
349
                }
350 22
351 13
                /** @psalm-suppress MissingClosureReturnType */
352
                return array_map(function ($value) use ($format, $level, $depth) {
353 9
                    return $this->exportJson($value, $format, $depth, $level + 1);
354
                }, $var);
355
            case 'object':
356
                if ($var instanceof Closure) {
357 6
                    return $this->exportClosure($var);
358
                }
359 6
360
                $objectClass = $this->getObjectClass($var);
361 6
                $objectId = $this->getObjectId($var);
362
                if ($depth <= $level) {
363
                    return [
364
                        self::OBJECT_ID_PROPERTY => $objectId,
365 6
                        self::OBJECT_CLASS_PROPERTY => $objectClass,
366 4
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
367
                    ];
368
                }
369 2
370
                $objectProperties = $this->getObjectProperties($var);
371
372
                $output = [
373
                    self::OBJECT_ID_PROPERTY => $objectId,
374
                    self::OBJECT_CLASS_PROPERTY => $objectClass,
375
                ];
376
                /** @psalm-var mixed $value */
377
                foreach ($objectProperties as $name => $value) {
378
                    $propertyNames = explode("\0", trim((string) $name));
379
                    $propertyName = end($propertyNames) ?: $name;
380
                    $output[$propertyName] = $this->exportJson($value, $format, $depth, $level + 1);
381 2
                }
382
                return $output ;
383 2
            default:
384
                return $var;
385
        }
386
    }
387 2
388
    private function getPropertyName(string $property): string
389
    {
390
        $property = str_replace("\0", '::', trim($property));
391 2
392
        if (str_starts_with($property, '*::')) {
393
            return substr($property, 3);
394
        }
395
396 2
        if (($pos = strpos($property, '::')) !== false) {
397
            return substr($property, $pos + 2);
398
        }
399
400 2
        return $property;
401
    }
402
403 6
    /**
404
     * @param object $variable
405 6
     * @param bool $format
406 6
     * @param int $level
407 6
     *
408 6
     * @throws ReflectionException
409 6
     *
410 6
     * @return string
411 2
     */
412 2
    private function exportObjectFallback(object $variable, bool $format, int $level): string
413 2
    {
414
        if ($variable instanceof ArrayableInterface) {
415
            return $this->exportInternal($variable->toArray(), $format, $level);
416 4
        }
417 4
418 4
        if ($variable instanceof JsonSerializable) {
419 4
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
420
        }
421
422 6
        if ($variable instanceof IteratorAggregate) {
423 6
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
424
        }
425 6
426
        /** @psalm-suppress RedundantCondition */
427
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
428
            return $this->exportVariable($variable->__toString());
429
        }
430
431
        return $this->exportVariable(self::create($variable)->asString());
432
    }
433 6
434 6
    private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string
435 6
    {
436 6
        $spaces = str_repeat($this->offset, $level);
437
        $objectProperties = $this->getObjectProperties($variable);
438
        $class = get_class($variable);
439 6
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(', ', $this->useVarInClosures) . ')';
440
        $lines = ['(static function ()' . $use . ' {',];
441
        if ($reflectionObject->getConstructor() === null) {
442
            $lines = array_merge($lines, [
443
                $this->offset . '$object = new ' . $class . '();',
444
                $this->offset . '(function ()' . $use . ' {',
445
            ]);
446
        } else {
447
            $lines = array_merge($lines, [
448
                $this->offset . '$class = new \ReflectionClass(\'' . $class . '\');',
449
                $this->offset . '$object = $class->newInstanceWithoutConstructor();',
450
                $this->offset . '(function ()' . $use . ' {',
451 46
            ]);
452
        }
453 46
        $endLines = [
454 1
            $this->offset . '})->bindTo($object, \'' . $class . '\')();',
455
            '',
456
            $this->offset . 'return $object;',
457 46
            '})()',
458
        ];
459
460
        /**
461
         * @psalm-var mixed $value
462
         * @psalm-var string $name
463
         */
464
        foreach ($objectProperties as $name => $value) {
465 43
            $propertyName = $this->getPropertyName($name);
466
            $lines[] = $this->offset . $this->offset . '$this->' . $propertyName . ' = ' .
467 43
                $this->exportInternal($value, $format, $level + 2) . ';';
468
        }
469
470 11
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
471
    }
472 11
473
    /**
474
     * Exports a {@see \Closure} instance.
475 16
     *
476
     * @param Closure $closure Closure instance.
477 16
     *
478
     * @throws ReflectionException
479 2
     *
480
     * @return string
481
     */
482 16
    private function exportClosure(Closure $closure, int $level = 0): string
483
    {
484
        if (self::$closureExporter === null) {
485
            self::$closureExporter = new ClosureExporter();
486
        }
487
488
        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

488
        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...
489
    }
490
491
    /**
492
     * @param mixed $variable
493
     *
494
     * @return string
495
     */
496
    private function exportVariable($variable): string
497
    {
498
        return var_export($variable, true);
499
    }
500
501
    private function getObjectDescription(object $object): string
502
    {
503
        return $this->getObjectClass($object) . '#' . $this->getObjectId($object);
504
    }
505
506
    private function getObjectClass(object $object): string
507
    {
508
        return get_class($object);
509
    }
510
511
    private function getObjectId(object $object): string
512
    {
513
        return (string) spl_object_id($object);
514
    }
515
516
    private function getObjectProperties(object $var): array
517
    {
518
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
519
            /** @var array $var */
520
            $var = $var->__debugInfo();
521
        }
522
523
        return (array) $var;
524
    }
525
}
526