Passed
Pull Request — master (#59)
by Fedonyuk
02:13
created

VarDumper::setOffset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

394
        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...
395
    }
396
397
    /**
398
     * @param mixed $variable
399
     *
400
     * @return string
401
     */
402 41
    private function exportVariable($variable): string
403
    {
404 41
        return var_export($variable, true);
405
    }
406
407 7
    private function getObjectDescription(object $object): string
408
    {
409 7
        return get_class($object) . '#' . spl_object_id($object);
410
    }
411
412 12
    private function getObjectProperties(object $var): array
413
    {
414 12
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
415
            /** @var array $var */
416 1
            $var = $var->__debugInfo();
417
        }
418
419 12
        return (array) $var;
420
    }
421
}
422