Passed
Pull Request — master (#54)
by Alexander
02:12
created

VarDumper::exportInternal()   D

Complexity

Conditions 20
Paths 33

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 20.2133

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

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