Passed
Push — master ( f42d48...6babd8 )
by Alexander
02:51
created

VarDumper::exportInternal()   D

Complexity

Conditions 20
Paths 33

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 20.0633

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 20
eloc 39
c 6
b 0
f 0
nc 33
nop 3
dl 0
loc 58
ccs 35
cts 37
cp 0.9459
crap 20.0633
rs 4.1666

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

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