Passed
Pull Request — master (#54)
by Alexander
02:12
created

VarDumper::exportObjectFallback()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 8.304

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 9
c 1
b 0
f 0
nc 5
nop 3
dl 0
loc 20
ccs 6
cts 10
cp 0.6
crap 8.304
rs 9.2222
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 JsonSerializable;
12
use ReflectionObject;
13
use ReflectionException;
14
use Yiisoft\Arrays\ArrayableInterface;
15
16
use function array_keys;
17
use function count;
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 range;
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
    /**
44
     * @var mixed Variable to dump.
45
     */
46
    private $variable;
47
    private array $useVarInClosures = [];
48
    private bool $serializeObjects = true;
49
    private static ?ClosureExporter $closureExporter = null;
50
51
    /**
52
     * @param mixed $variable Variable to dump.
53
     */
54 81
    private function __construct($variable)
55
    {
56 81
        $this->variable = $variable;
57 81
    }
58
59
    /**
60
     * @param mixed $variable Variable to dump.
61
     *
62
     * @return static An instance containing variable to dump.
63
     */
64 81
    public static function create($variable): self
65
    {
66 81
        return new self($variable);
67
    }
68
69
    /**
70
     * Prints a variable.
71
     *
72
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
73
     * but is more robust when handling complex objects.
74
     *
75
     * @param mixed $variable Variable to be dumped.
76
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
77
     * @param bool $highlight Whether the result should be syntax-highlighted.
78
     *
79
     * @throws ReflectionException
80
     */
81 6
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
82
    {
83 6
        echo self::create($variable)->asString($depth, $highlight);
84 6
    }
85
86
    /**
87
     * Dumps a variable in terms of a string.
88
     *
89
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
90
     * but is more robust when handling complex objects.
91
     *
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
     * @return string The string representation of the variable.* @return string The string representation of the variable.
98
     */
99 32
    public function asString(int $depth = 10, bool $highlight = false): string
100
    {
101 32
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
102
103 32
        if ($highlight) {
104 1
            $result = highlight_string("<?php\n" . $output, true);
105 1
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
106
        }
107
108 32
        return $output;
109
    }
110
111
    /**
112
     * Exports a variable as a string containing PHP code.
113
     *
114
     * The string is a valid PHP expression that can be evaluated by PHP parser
115
     * and the evaluation result will give back the variable value.
116
     *
117
     * This method is similar to {@see var_export()}. The main difference is that
118
     * it generates more compact string representation using short array syntax.
119
     *
120
     * It also handles closures with {@see ClosureExporter} and objects
121
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
122
     *
123
     * @param bool $format Whatever to format code.
124
     * @param array $useVarInClosures Array of variables used in `use` statement (['$params', '$config'])
125
     * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it
126
     * is false only objects of internal classes will be serialized.
127
     *
128
     * @throws ReflectionException
129
     *
130
     * @return string A PHP code representation of the variable.
131
     */
132 51
    public function export(bool $format = true, array $useVarInClosures = [], bool $serializeObjects = true): string
133
    {
134 51
        $this->useVarInClosures = $useVarInClosures;
135 51
        $this->serializeObjects = $serializeObjects;
136 51
        return $this->exportInternal($this->variable, $format, 0);
137
    }
138
139
    /**
140
     * @param mixed $var Variable to be dumped.
141
     * @param bool $format Whatever to format code.
142
     * @param int $depth Maximum depth.
143
     * @param int $level Current depth.
144
     *
145
     * @throws ReflectionException
146
     *
147
     * @return string
148
     */
149 32
    private function dumpInternal($var, bool $format, int $depth, int $level): string
150
    {
151 32
        switch (gettype($var)) {
152 32
            case 'resource':
153 31
            case 'resource (closed)':
154 1
                return '{resource}';
155 31
            case 'NULL':
156 1
                return 'null';
157 30
            case 'array':
158 6
                if ($depth <= $level) {
159 1
                    return '[...]';
160
                }
161
162 5
                if (empty($var)) {
163 2
                    return '[]';
164
                }
165
166 3
                $output = '';
167 3
                $keys = array_keys($var);
168 3
                $spaces = str_repeat(' ', $level * 4);
169 3
                $output .= '[';
170
171 3
                foreach ($keys as $name) {
172 3
                    if ($format) {
173 3
                        $output .= "\n" . $spaces . '    ';
174
                    }
175 3
                    $output .= $this->exportVariable($name);
176 3
                    $output .= ' => ';
177 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
178
                }
179
180 3
                return $format
181 3
                    ? $output . "\n" . $spaces . ']'
182 3
                    : $output . ']';
183 28
            case 'object':
184 16
                if ($var instanceof Closure) {
185 11
                    return $this->exportClosure($var);
186
                }
187
188 7
                if ($depth <= $level) {
189 1
                    return $this->getObjectDescription($var) . ' (...)';
190
                }
191
192 6
                $spaces = str_repeat(' ', $level * 4);
193 6
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
194 6
                $objectProperties = $this->getObjectProperties($var);
195
196
                /** @psalm-var mixed $value */
197 6
                foreach ($objectProperties as $name => $value) {
198 4
                    $propertyName = strtr(trim((string) $name), "\0", '::');
199 4
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
200 4
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
201
                }
202 6
                return $output . "\n" . $spaces . ')';
203
            default:
204 14
                return $this->exportVariable($var);
205
        }
206
    }
207
208
    /**
209
     * @param mixed $variable Variable to be exported.
210
     * @param bool $format Whatever to format code.
211
     * @param int $level Current depth.
212
     *
213
     * @throws ReflectionException
214
     *
215
     * @return string
216
     */
217 51
    private function exportInternal($variable, bool $format, int $level): string
218
    {
219 51
        $spaces = str_repeat(' ', $level * 4);
220 51
        switch (gettype($variable)) {
221 51
            case 'NULL':
222 2
                return 'null';
223 49
            case 'array':
224 9
                if (empty($variable)) {
225 2
                    return '[]';
226
                }
227
228 7
                $keys = array_keys($variable);
229 7
                $outputKeys = ($keys !== range(0, count($variable) - 1));
230 7
                $output = '[';
231
232 7
                foreach ($keys as $key) {
233 7
                    if ($format) {
234 4
                        $output .= "\n" . $spaces . '    ';
235
                    }
236 7
                    if ($outputKeys) {
237 3
                        $output .= $this->exportVariable($key);
238 3
                        $output .= ' => ';
239
                    }
240 7
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
241 7
                    if ($format || next($keys) !== false) {
242 6
                        $output .= ',';
243
                    }
244
                }
245
246 7
                return $format
247 4
                    ? $output . "\n" . $spaces . ']'
248 7
                    : $output . ']';
249 47
            case 'object':
250 30
                if ($variable instanceof Closure) {
251 24
                    return $this->exportClosure($variable, $level);
252
                }
253
254 10
                $reflectionObject = new ReflectionObject($variable);
255
                try {
256 10
                    if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) {
257 10
                        return "unserialize({$this->exportVariable(serialize($variable))})";
258
                    }
259
260
                    return $this->exportObject($variable, $reflectionObject, $format, $level);
261 6
                } catch (Exception $e) {
262
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
263 6
                    if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) {
264
                        try {
265 4
                            return $this->exportObject($variable, $reflectionObject, $format, $level);
266
                        } catch (Exception $e) {
267
                            return $this->exportObjectFallback($variable, $format, $level);
268
                        }
269
                    }
270
271 2
                    return $this->exportObjectFallback($variable, $format, $level);
272
                }
273
            default:
274 17
                return $this->exportVariable($variable);
275
        }
276
    }
277
278 4
    private function getPropertyName(string $property): string
279
    {
280 4
        $property = str_replace("\0", '::', trim($property));
281
282 4
        if (strpos($property, '*::') === 0) {
283
            return substr($property, 3);
284
        }
285
286 4
        if (($pos = strpos($property, '::')) !== false) {
287 4
            return substr($property, $pos + 2);
288
        }
289
290
        return $property;
291
    }
292
293
294
    /**
295
     * @param object $variable
296
     * @param bool $format
297
     * @param int $level
298
     *
299
     * @throws ReflectionException
300
     *
301
     * @return string
302
     */
303 2
    private function exportObjectFallback(object $variable, bool $format, int $level): string
304
    {
305 2
        if ($variable instanceof ArrayableInterface) {
306
            return $this->exportInternal($variable->toArray(), $format, $level);
307
        }
308
309 2
        if ($variable instanceof JsonSerializable) {
310
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
311
        }
312
313 2
        if ($variable instanceof IteratorAggregate) {
314
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
315
        }
316
317
        /** @psalm-suppress RedundantCondition */
318 2
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
319
            return $this->exportVariable($variable->__toString());
320
        }
321
322 2
        return $this->exportVariable(self::create($variable)->asString());
323
    }
324
325 4
    private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string
326
    {
327 4
        $spaces = str_repeat(' ', $level * 4);
328 4
        $objectProperties = $this->getObjectProperties($variable);
329 4
        $class = get_class($variable);
330 4
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(',', $this->useVarInClosures) . ')';
331 4
        $lines = ['(static function ()' . $use . ' {',];
332 4
        if ($reflectionObject->getConstructor() === null) {
333
            $lines = array_merge($lines, [
334
                '    $object = new ' . $class . '();',
335
                '    (function ()' . $use . ' {',
336
            ]);
337
        } else {
338 4
            $lines = array_merge($lines, [
339 4
                '    $class = new \ReflectionClass(\'' . $class . '\');',
340 4
                '    $object = $class->newInstanceWithoutConstructor();',
341 4
                '    (function ()' . $use . ' {',
342
            ]);
343
        }
344
        $endLines = [
345 4
            '    })->bindTo($object, \'' . $class . '\')();',
346 4
            '',
347 4
            '    return $object;',
348 4
            '})()',
349
        ];
350
351
        /**
352
         * @psalm-var mixed $value
353
         * @psalm-var string $name
354
         */
355 4
        foreach ($objectProperties as $name => $value) {
356 4
            $propertyName = $this->getPropertyName($name);
357 4
            $lines[] = '        $this->' . $propertyName . ' = ' .
358 4
                $this->exportInternal($value, $format, $level + 2) . ';';
359
        }
360
361 4
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
362
    }
363
364
    /**
365
     * Exports a {@see \Closure} instance.
366
     *
367
     * @param Closure $closure Closure instance.
368
     *
369
     * @throws ReflectionException
370
     *
371
     * @return string
372
     */
373 35
    private function exportClosure(Closure $closure, int $level = 0): string
374
    {
375 35
        if (self::$closureExporter === null) {
376 1
            self::$closureExporter = new ClosureExporter();
377
        }
378
379 35
        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

379
        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...
380
    }
381
382
    /**
383
     * @param mixed $variable
384
     *
385
     * @return string
386
     */
387 39
    private function exportVariable($variable): string
388
    {
389 39
        return var_export($variable, true);
390
    }
391
392 7
    private function getObjectDescription(object $object): string
393
    {
394 7
        return get_class($object) . '#' . spl_object_id($object);
395
    }
396
397 10
    private function getObjectProperties(object $var): array
398
    {
399 10
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
400
            /** @var array $var */
401 1
            $var = $var->__debugInfo();
402
        }
403
404 10
        return (array) $var;
405
    }
406
}
407