Test Failed
Pull Request — master (#94)
by Dmitriy
03:17 queued 16s
created

VarDumper::getDefaultHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

521
        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...
522
    }
523 40
524
    /**
525 40
     * @return string
526
     */
527
    private function exportVariable(mixed $variable): string
528 7
    {
529
        return var_export($variable, true);
530 7
    }
531
532
    private function getObjectDescription(object $object): string
533 23
    {
534
        return $object::class . '#' . $this->getObjectId($object);
535 23
    }
536
537
    private function getObjectId(object $object): string
538 28
    {
539
        return (string) spl_object_id($object);
540 28
    }
541
542 3
    private function getObjectProperties(object $var): array
543
    {
544
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
545 28
            /** @var array $var */
546
            $var = $var->__debugInfo();
547
        }
548 5
549
        return (array) $var;
550 5
    }
551 5
552 5
    private function exportDateTime(DateTimeInterface $variable): string
553 5
    {
554 5
        return sprintf(
555 5
            "new \%s('%s', new \DateTimeZone('%s'))",
556
            $variable::class,
557
            $variable->format(DateTimeInterface::RFC3339_EXTENDED),
558
            $variable->getTimezone()->getName()
559
        );
560
    }
561
}
562