Passed
Pull Request — master (#54)
by Dmitriy
07:53
created

VarDumper::exportObjectFallback()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 8.304

Importance

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

353
        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...
354
    }
355
356
    /**
357
     * @param mixed $variable
358
     *
359
     * @return string
360
     */
361 38
    private function exportVariable($variable): string
362
    {
363 38
        return var_export($variable, true);
364
    }
365
366 6
    private function getObjectDescription(object $object): string
367
    {
368 6
        return get_class($object) . '#' . spl_object_id($object);
369
    }
370
371 10
    private function getObjectProperties(object $var): array
372
    {
373 10
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
374
            /** @var array $var */
375 1
            $var = $var->__debugInfo();
376
        }
377
378 10
        return (array) $var;
379
    }
380
}
381