Passed
Pull Request — master (#59)
by Fedonyuk
02:17
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 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