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

522
        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...
523
    }
524
525
    /**
526
     * @param mixed $variable
527
     *
528
     * @return string
529
     */
530 41
    private function exportVariable($variable): string
531
    {
532 41
        return var_export($variable, true);
533
    }
534
535 7
    private function getObjectDescription(object $object): string
536
    {
537 7
        return get_class($object) . '#' . $this->getObjectId($object);
538
    }
539
540 21
    private function getObjectId(object $object): string
541
    {
542 21
        return (string) spl_object_id($object);
543
    }
544
545 26
    private function getObjectProperties(object $var): array
546
    {
547 26
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
548
            /** @var array $var */
549 3
            $var = $var->__debugInfo();
550
        }
551
552 26
        return (array) $var;
553
    }
554
}
555