Passed
Pull Request — master (#76)
by Dmitriy
02:13
created

VarDumper   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 513
Duplicated Lines 0 %

Test Coverage

Coverage 93.48%

Importance

Changes 28
Bugs 4 Features 0
Metric Value
eloc 196
c 28
b 4
f 0
dl 0
loc 513
ccs 172
cts 184
cp 0.9348
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
B exportPrimitives() 0 59 9
A getObjectId() 0 3 1
D exportInternal() 0 58 20
A asPrimitives() 0 3 1
A getPropertyName() 0 16 4
A exportVariable() 0 3 1
A getObjectProperties() 0 8 3
A asJson() 0 10 2
A dump() 0 3 1
A asString() 0 10 2
A exportObject() 0 37 5
A exportObjectFallback() 0 20 6
C dumpInternal() 0 56 14
A export() 0 5 1
A exportClosure() 0 7 2
A getObjectDescription() 0 3 1

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

522
        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...
523
    }
524
525
    /**
526
     * @param mixed $variable
527
     *
528
     * @return string
529
     */
530 41
    private function exportVariable($variable): string
531
    {
532 41
        return var_export($variable, true);
533
    }
534
535 7
    private function getObjectDescription(object $object): string
536
    {
537 7
        return get_class($object) . '#' . $this->getObjectId($object);
538
    }
539
540 21
    private function getObjectId(object $object): string
541
    {
542 21
        return (string) spl_object_id($object);
543
    }
544
545 26
    private function getObjectProperties(object $var): array
546
    {
547 26
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
548
            /** @var array $var */
549 3
            $var = $var->__debugInfo();
550
        }
551
552 26
        return (array) $var;
553
    }
554
}
555