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

VarDumper::asJson()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 10
ccs 4
cts 5
cp 0.8
crap 2.032
rs 10
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