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

489
        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...
490
    }
491
492
    /**
493
     * @param mixed $variable
494
     *
495
     * @return string
496
     */
497 41
    private function exportVariable($variable): string
498
    {
499 41
        return var_export($variable, true);
500
    }
501
502 7
    private function getObjectDescription(object $object): string
503
    {
504 7
        return $this->getObjectClass($object) . '#' . $this->getObjectId($object);
505
    }
506
507 11
    private function getObjectClass(object $object): string
508
    {
509 11
        return get_class($object);
510
    }
511
512 11
    private function getObjectId(object $object): string
513
    {
514 11
        return (string) spl_object_id($object);
515
    }
516
517 16
    private function getObjectProperties(object $var): array
518
    {
519 16
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
520
            /** @var array $var */
521 2
            $var = $var->__debugInfo();
522
        }
523
524 16
        return (array) $var;
525
    }
526
}
527