Test Failed
Push — master ( 427759...b5d2db )
by
unknown
02:24
created

VarDumper::exportJson()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10.2918

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 32
c 1
b 0
f 0
nc 9
nop 4
dl 0
loc 47
ccs 18
cts 21
cp 0.8571
crap 10.2918
rs 7.6666

How to fix   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 107
    /**
61
     * @param mixed $variable Variable to dump.
62 107
     */
63
    private function __construct($variable)
64
    {
65
        $this->variable = $variable;
66
    }
67
68
    /**
69
     * @param mixed $variable Variable to dump.
70 107
     *
71
     * @return static An instance containing variable to dump.
72 107
     */
73
    public static function create($variable): self
74
    {
75
        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 6
     *
88
     * @throws ReflectionException
89 6
     */
90
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
91
    {
92
        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 32
     *
121
     * @return string The string representation of the variable.
122 32
     */
123
    public function asString(int $depth = 10, bool $highlight = false): string
124 32
    {
125 1
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
126 1
127
        if ($highlight) {
128
            $result = highlight_string("<?php\n" . $output, true);
129 32
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
130
        }
131
132
        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 24
     *
146
     * @return string The json representation of the variable.
147
     */
148 24
    public function asJson(bool $format = true, int $depth = 10): string
149
    {
150 24
        /** @var mixed $output */
151 24
        $output = $this->exportJson($this->variable, $format, $depth, 0);
152
153
        if ($format) {
154
            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 53
     *
179
     * @return string A PHP code representation of the variable.
180 53
     */
181 53
    public function export(bool $format = true, array $useVariables = [], bool $serializeObjects = true): string
182 53
    {
183
        $this->useVarInClosures = $useVariables;
184
        $this->serializeObjects = $serializeObjects;
185
        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 45
     *
196
     * @return string
197 45
     */
198 45
    private function dumpInternal($var, bool $format, int $depth, int $level): string
199 44
    {
200 1
        switch (gettype($var)) {
201 44
            case 'resource':
202 1
            case 'resource (closed)':
203 43
                return '{resource}';
204 6
            case 'NULL':
205 1
                return 'null';
206
            case 'array':
207
                if ($depth <= $level) {
208 5
                    return '[...]';
209 2
                }
210
211
                if (empty($var)) {
212 3
                    return '[]';
213 3
                }
214 3
215 3
                $output = '';
216
                $keys = array_keys($var);
217 3
                $spaces = str_repeat($this->offset, $level);
218 3
                $output .= '[';
219 3
220
                foreach ($keys as $name) {
221 3
                    if ($format) {
222 3
                        $output .= "\n" . $spaces . $this->offset;
223 3
                    }
224
                    $output .= $this->exportVariable($name);
225
                    $output .= ' => ';
226 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
227 3
                }
228 3
229 41
                return $format
230 29
                    ? $output . "\n" . $spaces . ']'
231 21
                    : $output . ']';
232
            case 'object':
233
                if ($var instanceof Closure) {
234 11
                    return $this->exportClosure($var);
235 1
                }
236
237
                if ($depth <= $level) {
238 10
                    return $this->getObjectDescription($var) . ' (...)';
239 10
                }
240 10
241
                $spaces = str_repeat($this->offset, $level);
242
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
243 10
                $objectProperties = $this->getObjectProperties($var);
244 7
245 7
                /** @psalm-var mixed $value */
246 7
                foreach ($objectProperties as $name => $value) {
247
                    $propertyName = strtr(trim((string) $name), "\0", '::');
248 10
                    $output .= "\n" . $spaces . $this->offset . '[' . $propertyName . '] => ';
249
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
250 16
                }
251
                return $output . "\n" . $spaces . ')';
252
            default:
253
                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 53
     *
264
     * @return string
265 53
     */
266 53
    private function exportInternal($variable, bool $format, int $level): string
267 53
    {
268 2
        $spaces = str_repeat($this->offset, $level);
269 51
        switch (gettype($variable)) {
270 9
            case 'NULL':
271 2
                return 'null';
272
            case 'array':
273
                if (empty($variable)) {
274 7
                    return '[]';
275 7
                }
276 7
277
                $keys = array_keys($variable);
278 7
                $outputKeys = $keys !== array_keys($keys);
279 7
                $output = '[';
280 4
281
                foreach ($keys as $key) {
282 7
                    if ($format) {
283 3
                        $output .= "\n" . $spaces . $this->offset;
284 3
                    }
285
                    if ($outputKeys) {
286 7
                        $output .= $this->exportVariable($key);
287 7
                        $output .= ' => ';
288 6
                    }
289
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
290
                    if ($format || next($keys) !== false) {
291
                        $output .= ',';
292 7
                    }
293 4
                }
294 7
295 49
                return $format
296 32
                    ? $output . "\n" . $spaces . ']'
297 25
                    : $output . ']';
298
            case 'object':
299
                if ($variable instanceof Closure) {
300 12
                    return $this->exportClosure($variable, $level);
301
                }
302 12
303 10
                $reflectionObject = new ReflectionObject($variable);
304
                try {
305
                    if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) {
306 2
                        return "unserialize({$this->exportVariable(serialize($variable))})";
307 6
                    }
308
309 6
                    return $this->exportObject($variable, $reflectionObject, $format, $level);
310
                } catch (Exception $e) {
311 4
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
312
                    if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) {
313
                        try {
314
                            return $this->exportObject($variable, $reflectionObject, $format, $level);
315
                        } catch (Exception $e) {
316
                            return $this->exportObjectFallback($variable, $format, $level);
317 2
                        }
318
                    }
319
320 19
                    return $this->exportObjectFallback($variable, $format, $level);
321
                }
322
            default:
323
                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 24
     * @return mixed
336
     * @psalm-param mixed $var
337 24
     */
338 24
    private function exportJson($var, bool $format, int $depth, int $level)
339 23
    {
340 1
        switch (gettype($var)) {
341 23
            case 'resource':
342 4
            case 'resource (closed)':
343
                return '{resource}';
344
            case 'array':
345
                if ($depth <= $level) {
346
                    return [
347 4
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
348 3
                    ];
349
                }
350 22
351 13
                /** @psalm-suppress MissingClosureReturnType */
352
                return array_map(function ($value) use ($format, $level, $depth) {
353 9
                    return $this->exportJson($value, $format, $depth, $level + 1);
354
                }, $var);
355
            case 'object':
356
                if ($var instanceof Closure) {
357 6
                    return $this->exportClosure($var);
358
                }
359 6
360
                $objectClass = $this->getObjectClass($var);
361 6
                $objectId = $this->getObjectId($var);
362
                if ($depth <= $level) {
363
                    return [
364
                        self::OBJECT_ID_PROPERTY => $objectId,
365 6
                        self::OBJECT_CLASS_PROPERTY => $objectClass,
366 4
                        self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true,
367
                    ];
368
                }
369 2
370
                $objectProperties = $this->getObjectProperties($var);
371
372
                $output = [
373
                    self::OBJECT_ID_PROPERTY => $objectId,
374
                    self::OBJECT_CLASS_PROPERTY => $objectClass,
375
                ];
376
                /** @psalm-var mixed $value */
377
                foreach ($objectProperties as $name => $value) {
378
                    $propertyNames = explode("\0", trim((string) $name));
379
                    $propertyName = end($propertyNames) ?: $name;
380
                    $output[$propertyName] = $this->exportJson($value, $format, $depth, $level + 1);
381 2
                }
382
                return $output ;
383 2
            default:
384
                return $var;
385
        }
386
    }
387 2
388
    private function getPropertyName(string $property): string
389
    {
390
        $property = str_replace("\0", '::', trim($property));
391 2
392
        if (str_starts_with($property, '*::')) {
393
            return substr($property, 3);
394
        }
395
396 2
        if (($pos = strpos($property, '::')) !== false) {
397
            return substr($property, $pos + 2);
398
        }
399
400 2
        return $property;
401
    }
402
403 6
    /**
404
     * @param object $variable
405 6
     * @param bool $format
406 6
     * @param int $level
407 6
     *
408 6
     * @throws ReflectionException
409 6
     *
410 6
     * @return string
411 2
     */
412 2
    private function exportObjectFallback(object $variable, bool $format, int $level): string
413 2
    {
414
        if ($variable instanceof ArrayableInterface) {
415
            return $this->exportInternal($variable->toArray(), $format, $level);
416 4
        }
417 4
418 4
        if ($variable instanceof JsonSerializable) {
419 4
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
420
        }
421
422 6
        if ($variable instanceof IteratorAggregate) {
423 6
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
424
        }
425 6
426
        /** @psalm-suppress RedundantCondition */
427
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
428
            return $this->exportVariable($variable->__toString());
429
        }
430
431
        return $this->exportVariable(self::create($variable)->asString());
432
    }
433 6
434 6
    private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string
435 6
    {
436 6
        $spaces = str_repeat($this->offset, $level);
437
        $objectProperties = $this->getObjectProperties($variable);
438
        $class = get_class($variable);
439 6
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(', ', $this->useVarInClosures) . ')';
440
        $lines = ['(static function ()' . $use . ' {',];
441
        if ($reflectionObject->getConstructor() === null) {
442
            $lines = array_merge($lines, [
443
                $this->offset . '$object = new ' . $class . '();',
444
                $this->offset . '(function ()' . $use . ' {',
445
            ]);
446
        } else {
447
            $lines = array_merge($lines, [
448
                $this->offset . '$class = new \ReflectionClass(\'' . $class . '\');',
449
                $this->offset . '$object = $class->newInstanceWithoutConstructor();',
450
                $this->offset . '(function ()' . $use . ' {',
451 46
            ]);
452
        }
453 46
        $endLines = [
454 1
            $this->offset . '})->bindTo($object, \'' . $class . '\')();',
455
            '',
456
            $this->offset . 'return $object;',
457 46
            '})()',
458
        ];
459
460
        /**
461
         * @psalm-var mixed $value
462
         * @psalm-var string $name
463
         */
464
        foreach ($objectProperties as $name => $value) {
465 43
            $propertyName = $this->getPropertyName($name);
466
            $lines[] = $this->offset . $this->offset . '$this->' . $propertyName . ' = ' .
467 43
                $this->exportInternal($value, $format, $level + 2) . ';';
468
        }
469
470 11
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
471
    }
472 11
473
    /**
474
     * Exports a {@see \Closure} instance.
475 16
     *
476
     * @param Closure $closure Closure instance.
477 16
     *
478
     * @throws ReflectionException
479 2
     *
480
     * @return string
481
     */
482 16
    private function exportClosure(Closure $closure, int $level = 0): string
483
    {
484
        if (self::$closureExporter === null) {
485
            self::$closureExporter = new ClosureExporter();
486
        }
487
488
        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

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