Passed
Push — master ( a53302...e71c1f )
by Alexander
06:09 queued 03:53
created

VarDumper::exportInternal()   D

Complexity

Conditions 20
Paths 33

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 20.0633

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 20
eloc 39
c 6
b 0
f 0
nc 33
nop 3
dl 0
loc 58
ccs 35
cts 37
cp 0.9459
crap 20.0633
rs 4.1666

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 __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
        /** @var mixed $output */
148 24
        $output = $this->exportJson($this->variable, $format, $depth, 0);
149
150 24
        if ($format) {
151 24
            return json_encode($output, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
152
        }
153
154
        return json_encode($output, JSON_THROW_ON_ERROR);
155
    }
156
157
    /**
158
     * Exports a variable as a string containing PHP code.
159
     *
160
     * The string is a valid PHP expression that can be evaluated by PHP parser
161
     * and the evaluation result will give back the variable value.
162
     *
163
     * This method is similar to {@see var_export()}. The main difference is that
164
     * it generates more compact string representation using short array syntax.
165
     *
166
     * It also handles closures with {@see ClosureExporter} and objects
167
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
168
     *
169
     * @param bool $format Whatever to format code.
170
     * @param string[] $useVariables Array of variables used in `use` statement (['$params', '$config'])
171
     * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it
172
     * is false only objects of internal classes will be serialized.
173
     *
174
     * @throws ReflectionException
175
     *
176
     * @return string A PHP code representation of the variable.
177
     */
178 53
    public function export(bool $format = true, array $useVariables = [], bool $serializeObjects = true): string
179
    {
180 53
        $this->useVarInClosures = $useVariables;
181 53
        $this->serializeObjects = $serializeObjects;
182 53
        return $this->exportInternal($this->variable, $format, 0);
183
    }
184
185
    /**
186
     * @param mixed $var Variable to be dumped.
187
     * @param bool $format Whatever to format code.
188
     * @param int $depth Maximum depth.
189
     * @param int $level Current depth.
190
     *
191
     * @throws ReflectionException
192
     *
193
     * @return string
194
     */
195 45
    private function dumpInternal($var, bool $format, int $depth, int $level): string
196
    {
197 45
        switch (gettype($var)) {
198 45
            case 'resource':
199 44
            case 'resource (closed)':
200 1
                return '{resource}';
201 44
            case 'NULL':
202 1
                return 'null';
203 43
            case 'array':
204 6
                if ($depth <= $level) {
205 1
                    return '[...]';
206
                }
207
208 5
                if (empty($var)) {
209 2
                    return '[]';
210
                }
211
212 3
                $output = '';
213 3
                $keys = array_keys($var);
214 3
                $spaces = str_repeat($this->offset, $level);
215 3
                $output .= '[';
216
217 3
                foreach ($keys as $name) {
218 3
                    if ($format) {
219 3
                        $output .= "\n" . $spaces . $this->offset;
220
                    }
221 3
                    $output .= $this->exportVariable($name);
222 3
                    $output .= ' => ';
223 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
224
                }
225
226 3
                return $format
227 3
                    ? $output . "\n" . $spaces . ']'
228 3
                    : $output . ']';
229 41
            case 'object':
230 29
                if ($var instanceof Closure) {
231 21
                    return $this->exportClosure($var);
232
                }
233
234 11
                if ($depth <= $level) {
235 1
                    return $this->getObjectDescription($var) . ' (...)';
236
                }
237
238 10
                $spaces = str_repeat($this->offset, $level);
239 10
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
240 10
                $objectProperties = $this->getObjectProperties($var);
241
242
                /** @psalm-var mixed $value */
243 10
                foreach ($objectProperties as $name => $value) {
244 7
                    $propertyName = strtr(trim((string) $name), "\0", '::');
245 7
                    $output .= "\n" . $spaces . $this->offset . '[' . $propertyName . '] => ';
246 7
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
247
                }
248 10
                return $output . "\n" . $spaces . ')';
249
            default:
250 16
                return $this->exportVariable($var);
251
        }
252
    }
253
254
    /**
255
     * @param mixed $variable Variable to be exported.
256
     * @param bool $format Whatever to format code.
257
     * @param int $level Current depth.
258
     *
259
     * @throws ReflectionException
260
     *
261
     * @return string
262
     */
263 53
    private function exportInternal($variable, bool $format, int $level): string
264
    {
265 53
        $spaces = str_repeat($this->offset, $level);
266 53
        switch (gettype($variable)) {
267 53
            case 'NULL':
268 2
                return 'null';
269 51
            case 'array':
270 9
                if (empty($variable)) {
271 2
                    return '[]';
272
                }
273
274 7
                $keys = array_keys($variable);
275 7
                $outputKeys = $keys !== array_keys($keys);
276 7
                $output = '[';
277
278 7
                foreach ($keys as $key) {
279 7
                    if ($format) {
280 4
                        $output .= "\n" . $spaces . $this->offset;
281
                    }
282 7
                    if ($outputKeys) {
283 3
                        $output .= $this->exportVariable($key);
284 3
                        $output .= ' => ';
285
                    }
286 7
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
287 7
                    if ($format || next($keys) !== false) {
288 6
                        $output .= ',';
289
                    }
290
                }
291
292 7
                return $format
293 4
                    ? $output . "\n" . $spaces . ']'
294 7
                    : $output . ']';
295 49
            case 'object':
296 32
                if ($variable instanceof Closure) {
297 25
                    return $this->exportClosure($variable, $level);
298
                }
299
300 12
                $reflectionObject = new ReflectionObject($variable);
301
                try {
302 12
                    if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) {
303 10
                        return "unserialize({$this->exportVariable(serialize($variable))})";
304
                    }
305
306 2
                    return $this->exportObject($variable, $reflectionObject, $format, $level);
307 6
                } catch (Exception $e) {
308
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
309 6
                    if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) {
310
                        try {
311 4
                            return $this->exportObject($variable, $reflectionObject, $format, $level);
312
                        } catch (Exception $e) {
313
                            return $this->exportObjectFallback($variable, $format, $level);
314
                        }
315
                    }
316
317 2
                    return $this->exportObjectFallback($variable, $format, $level);
318
                }
319
            default:
320 19
                return $this->exportVariable($variable);
321
        }
322
    }
323
324
    /**
325
     * @param mixed $var
326
     * @param bool $format
327
     * @param int $depth
328
     * @param int $level
329
     * @return mixed
330
     * @throws ReflectionException
331
     * @psalm-param mixed $var
332
     */
333 24
    private function exportJson($var, bool $format, int $depth, int $level)
334
    {
335 24
        switch (gettype($var)) {
336 24
            case 'resource':
337 23
            case 'resource (closed)':
338 1
                return '{resource}';
339 23
            case 'array':
340 4
                if ($depth <= $level) {
341
                    return '[...]';
342
                }
343
344
                /** @psalm-suppress MissingClosureReturnType */
345 4
                return array_map(function ($value) use ($format, $level, $depth) {
346 3
                    return $this->exportJson($value, $format, $depth, $level + 1);
347
                }, $var);
348 22
            case 'object':
349 13
                return $this->dumpInternal($var, $format, $depth, $level);
350
            default:
351 9
                return $var;
352
        }
353
    }
354
355 6
    private function getPropertyName(string $property): string
356
    {
357 6
        $property = str_replace("\0", '::', trim($property));
358
359 6
        if (strpos($property, '*::') === 0) {
360
            return substr($property, 3);
361
        }
362
363 6
        if (($pos = strpos($property, '::')) !== false) {
364 4
            return substr($property, $pos + 2);
365
        }
366
367 2
        return $property;
368
    }
369
370
    /**
371
     * @param object $variable
372
     * @param bool $format
373
     * @param int $level
374
     *
375
     * @throws ReflectionException
376
     *
377
     * @return string
378
     */
379 2
    private function exportObjectFallback(object $variable, bool $format, int $level): string
380
    {
381 2
        if ($variable instanceof ArrayableInterface) {
382
            return $this->exportInternal($variable->toArray(), $format, $level);
383
        }
384
385 2
        if ($variable instanceof JsonSerializable) {
386
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
387
        }
388
389 2
        if ($variable instanceof IteratorAggregate) {
390
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
391
        }
392
393
        /** @psalm-suppress RedundantCondition */
394 2
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
395
            return $this->exportVariable($variable->__toString());
396
        }
397
398 2
        return $this->exportVariable(self::create($variable)->asString());
399
    }
400
401 6
    private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string
402
    {
403 6
        $spaces = str_repeat($this->offset, $level);
404 6
        $objectProperties = $this->getObjectProperties($variable);
405 6
        $class = get_class($variable);
406 6
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(', ', $this->useVarInClosures) . ')';
407 6
        $lines = ['(static function ()' . $use . ' {',];
408 6
        if ($reflectionObject->getConstructor() === null) {
409 2
            $lines = array_merge($lines, [
410 2
                $this->offset . '$object = new ' . $class . '();',
411 2
                $this->offset . '(function ()' . $use . ' {',
412
            ]);
413
        } else {
414 4
            $lines = array_merge($lines, [
415 4
                $this->offset . '$class = new \ReflectionClass(\'' . $class . '\');',
416 4
                $this->offset . '$object = $class->newInstanceWithoutConstructor();',
417 4
                $this->offset . '(function ()' . $use . ' {',
418
            ]);
419
        }
420 6
        $endLines = [
421 6
            $this->offset . '})->bindTo($object, \'' . $class . '\')();',
422
            '',
423 6
            $this->offset . 'return $object;',
424
            '})()',
425
        ];
426
427
        /**
428
         * @psalm-var mixed $value
429
         * @psalm-var string $name
430
         */
431 6
        foreach ($objectProperties as $name => $value) {
432 6
            $propertyName = $this->getPropertyName($name);
433 6
            $lines[] = $this->offset . $this->offset . '$this->' . $propertyName . ' = ' .
434 6
                $this->exportInternal($value, $format, $level + 2) . ';';
435
        }
436
437 6
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
438
    }
439
440
    /**
441
     * Exports a {@see \Closure} instance.
442
     *
443
     * @param Closure $closure Closure instance.
444
     *
445
     * @throws ReflectionException
446
     *
447
     * @return string
448
     */
449 46
    private function exportClosure(Closure $closure, int $level = 0): string
450
    {
451 46
        if (self::$closureExporter === null) {
452 1
            self::$closureExporter = new ClosureExporter();
453
        }
454
455 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

455
        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...
456
    }
457
458
    /**
459
     * @param mixed $variable
460
     *
461
     * @return string
462
     */
463 43
    private function exportVariable($variable): string
464
    {
465 43
        return var_export($variable, true);
466
    }
467
468 11
    private function getObjectDescription(object $object): string
469
    {
470 11
        return get_class($object) . '#' . spl_object_id($object);
471
    }
472
473 16
    private function getObjectProperties(object $var): array
474
    {
475 16
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
476
            /** @var array $var */
477 2
            $var = $var->__debugInfo();
478
        }
479
480 16
        return (array) $var;
481
    }
482
}
483