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

VarDumper::dumpInternal()   C

Complexity

Conditions 14
Paths 16

Size

Total Lines 56
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 14

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 14
eloc 39
c 7
b 0
f 0
nc 16
nop 4
dl 0
loc 56
ccs 39
cts 39
cp 1
crap 14
rs 6.2666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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