Passed
Push — master ( 47fe5a...924121 )
by Alexander
02:41
created

VarDumper   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Test Coverage

Coverage 87.94%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 185
c 5
b 0
f 0
dl 0
loc 402
ccs 175
cts 199
cp 0.8794
rs 2
wmc 80

20 Methods

Rating   Name   Duplication   Size   Complexity  
A dump() 0 3 1
A __construct() 0 3 1
A create() 0 3 1
A buildObjectsCache() 0 15 6
A asJsonObjectsMap() 0 5 1
A asJson() 0 3 1
A asString() 0 10 2
A dumpNested() 0 4 1
A export() 0 3 1
A normalizeProperty() 0 13 3
D exportInternal() 0 60 18
A getObjectProperties() 0 7 3
D dumpInternal() 0 66 20
A getResourceDescription() 0 10 2
A asPhpString() 0 4 1
A asJsonInternal() 0 9 2
A exportVariable() 0 3 1
A exportClosure() 0 7 2
A getObjectDescription() 0 3 1
C dumpNestedInternal() 0 60 12

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

394
        return $this->closureExporter->/** @scrutinizer ignore-call */ export($closure);

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...
395
    }
396
397 19
    public function asPhpString(): string
398
    {
399 19
        $this->beautify = false;
400 19
        return $this->export();
401
    }
402
403 18
    private function getObjectDescription(object $object): string
404
    {
405 18
        return get_class($object) . '#' . spl_object_id($object);
406
    }
407
408 22
    private function exportVariable($variable): string
409
    {
410 22
        return var_export($variable, true);
411
    }
412
413 25
    private function asJsonInternal($variable, bool $prettyPrint, int $depth, int $objectCollapseLevel)
414
    {
415 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
416
417 25
        if ($prettyPrint) {
418
            $options |= JSON_PRETTY_PRINT;
419
        }
420
421 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
422
    }
423
}
424