Passed
Push — master ( d478f0...a9f296 )
by Dmitriy
03:36 queued 01:25
created

VarDumper::exportDateTime()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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

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