Passed
Pull Request — master (#65)
by
unknown
13:51
created

VarDumper   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 430
Duplicated Lines 0 %

Test Coverage

Coverage 92.12%

Importance

Changes 16
Bugs 2 Features 0
Metric Value
eloc 162
c 16
b 2
f 0
dl 0
loc 430
ccs 152
cts 165
cp 0.9212
rs 2.8
wmc 70

17 Methods

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

444
        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...
445
    }
446
447
    /**
448
     * @param mixed $variable
449
     *
450
     * @return string
451
     */
452 43
    private function exportVariable($variable): string
453
    {
454 43
        return var_export($variable, true);
455
    }
456
457 11
    private function getObjectDescription(object $object): string
458
    {
459 11
        return get_class($object) . '#' . spl_object_id($object);
460
    }
461
462 16
    private function getObjectProperties(object $var): array
463
    {
464 16
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
465
            /** @var array $var */
466 2
            $var = $var->__debugInfo();
467
        }
468
469 16
        return (array) $var;
470
    }
471
}
472