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

505
        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...
506
    }
507
508
    /**
509
     * @param mixed $variable
510
     *
511
     * @return string
512
     */
513 41
    private function exportVariable($variable): string
514
    {
515 41
        return var_export($variable, true);
516
    }
517
518 7
    private function getObjectDescription(object $object): string
519
    {
520 7
        return get_class($object) . '#' . $this->getObjectId($object);
521
    }
522
523 21
    private function getObjectId(object $object): string
524
    {
525 21
        return (string) spl_object_id($object);
526
    }
527
528 26
    private function getObjectProperties(object $var): array
529
    {
530 26
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
531
            /** @var array $var */
532 3
            $var = $var->__debugInfo();
533
        }
534
535 26
        return (array) $var;
536
    }
537
}
538