Passed
Push — master ( f42d48...6babd8 )
by Alexander
02:51
created

VarDumper   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Test Coverage

Coverage 92.67%

Importance

Changes 15
Bugs 2 Features 0
Metric Value
eloc 144
c 15
b 2
f 0
dl 0
loc 384
ccs 139
cts 150
cp 0.9267
rs 3.44
wmc 62

15 Methods

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

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

397
        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...
398
    }
399
400
    /**
401
     * @param mixed $variable
402
     *
403
     * @return string
404
     */
405 41
    private function exportVariable($variable): string
406
    {
407 41
        return var_export($variable, true);
408
    }
409
410 7
    private function getObjectDescription(object $object): string
411
    {
412 7
        return get_class($object) . '#' . spl_object_id($object);
413
    }
414
415 12
    private function getObjectProperties(object $var): array
416
    {
417 12
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
418
            /** @var array $var */
419 1
            $var = $var->__debugInfo();
420
        }
421
422 12
        return (array) $var;
423
    }
424
}
425