Passed
Pull Request — master (#34)
by Evgeniy
01:51
created

VarDumper::dumpNestedInternal()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 61
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 13.0711

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 13
eloc 39
c 4
b 0
f 0
nc 10
nop 4
dl 0
loc 61
ccs 37
cts 40
cp 0.925
crap 13.0711
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

416
        return self::$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...
417
    }
418
419 18
    private function getObjectDescription(object $object): string
420
    {
421 18
        return get_class($object) . '#' . spl_object_id($object);
422
    }
423
424 33
    private function exportVariable($variable): string
425
    {
426 33
        return var_export($variable, true);
427
    }
428
429 25
    private function asJsonInternal($variable, bool $format, int $depth, int $objectCollapseLevel)
430
    {
431 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
432
433 25
        if ($format) {
434
            $options |= JSON_PRETTY_PRINT;
435
        }
436
437 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
438
    }
439
}
440