Passed
Pull Request — master (#54)
by Alexander
13:02 queued 20s
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 ReflectionClass;
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
     * @return string The string representation of the variable.
96
     * @throws ReflectionException
97
     */
98 31
    public function asString(int $depth = 10, bool $highlight = false): string
99
    {
100 31
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
101
102 31
        if ($highlight) {
103 1
            $result = highlight_string("<?php\n" . $output, true);
104 1
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
105
        }
106
107 31
        return $output;
108
    }
109
110
    /**
111
     * Exports a variable as a string containing PHP code.
112
     *
113
     * The string is a valid PHP expression that can be evaluated by PHP parser
114
     * and the evaluation result will give back the variable value.
115
     *
116
     * This method is similar to {@see var_export()}. The main difference is that
117
     * it generates more compact string representation using short array syntax.
118
     *
119
     * It also handles closures with {@see ClosureExporter} and objects
120
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
121
     *
122
     * @param bool $format Whatever to format code.
123
     * @param array $useVarInClosures Array of variables used in `use` statement (['$params', '$config'])
124
     * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it
125
     * is false only objects of internal classes will be serialized.
126
     *
127
     * @return string A PHP code representation of the variable.
128
     * @throws ReflectionException
129
     *
130
     */
131 51
    public function export(bool $format = true, array $useVarInClosures = [], bool $serializeObjects = true): string
132
    {
133 51
        $this->useVarInClosures = $useVarInClosures;
134 51
        $this->serializeObjects = $serializeObjects;
135 51
        return $this->exportInternal($this->variable, $format, 0);
136
    }
137
138
    /**
139
     * @param mixed $var Variable to be dumped.
140
     * @param bool $format Whatever to format code.
141
     * @param int $depth Maximum depth.
142
     * @param int $level Current depth.
143
     *
144
     * @return string
145
     * @throws ReflectionException
146
     *
147
     */
148 31
    private function dumpInternal($var, bool $format, int $depth, int $level): string
149
    {
150 31
        switch (gettype($var)) {
151 31
            case 'resource':
152 30
            case 'resource (closed)':
153 1
                return '{resource}';
154 30
            case 'NULL':
155 1
                return 'null';
156 29
            case 'array':
157 6
                if ($depth <= $level) {
158 1
                    return '[...]';
159
                }
160
161 5
                if (empty($var)) {
162 2
                    return '[]';
163
                }
164
165 3
                $output = '';
166 3
                $keys = array_keys($var);
167 3
                $spaces = str_repeat(' ', $level * 4);
168 3
                $output .= '[';
169
170 3
                foreach ($keys as $name) {
171 3
                    if ($format) {
172 3
                        $output .= "\n" . $spaces . '    ';
173
                    }
174 3
                    $output .= $this->exportVariable($name);
175 3
                    $output .= ' => ';
176 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
177
                }
178
179 3
                return $format
180 3
                    ? $output . "\n" . $spaces . ']'
181 3
                    : $output . ']';
182 27
            case 'object':
183 15
                if ($var instanceof Closure) {
184 10
                    return $this->exportClosure($var);
185
                }
186
187 6
                if ($depth <= $level) {
188 1
                    return $this->getObjectDescription($var) . ' (...)';
189
                }
190
191 5
                $spaces = str_repeat(' ', $level * 4);
192 5
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
193 5
                $objectProperties = $this->getObjectProperties($var);
194
195
                /** @psalm-var mixed $value */
196 5
                foreach ($objectProperties as $name => $value) {
197 3
                    $propertyName = strtr(trim((string) $name), "\0", '::');
198 3
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
199 3
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
200
                }
201 5
                return $output . "\n" . $spaces . ')';
202
            default:
203 14
                return $this->exportVariable($var);
204
        }
205
    }
206
207
    /**
208
     * @param mixed $variable Variable to be exported.
209
     * @param bool $format Whatever to format code.
210
     * @param int $level Current depth.
211
     *
212
     * @return string
213
     * @throws ReflectionException
214
     *
215
     */
216 51
    private function exportInternal($variable, bool $format, int $level): string
217
    {
218 51
        $spaces = str_repeat(' ', $level * 4);
219 51
        switch (gettype($variable)) {
220 51
            case 'NULL':
221 2
                return 'null';
222 49
            case 'array':
223 9
                if (empty($variable)) {
224 2
                    return '[]';
225
                }
226
227 7
                $keys = array_keys($variable);
228 7
                $outputKeys = ($keys !== range(0, count($variable) - 1));
229 7
                $output = '[';
230
231 7
                foreach ($keys as $key) {
232 7
                    if ($format) {
233 4
                        $output .= "\n" . $spaces . '    ';
234
                    }
235 7
                    if ($outputKeys) {
236 3
                        $output .= $this->exportVariable($key);
237 3
                        $output .= ' => ';
238
                    }
239 7
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
240 7
                    if ($format || next($keys) !== false) {
241 6
                        $output .= ',';
242
                    }
243
                }
244
245 7
                return $format
246 4
                    ? $output . "\n" . $spaces . ']'
247 7
                    : $output . ']';
248 47
            case 'object':
249 30
                if ($variable instanceof Closure) {
250 25
                    return $this->exportClosure($variable, $level);
251
                }
252
253 10
                $reflectionClass = new ReflectionClass($variable);
254
                try {
255 10
                    if ($this->serializeObjects || $reflectionClass->isInternal() || $reflectionClass->isAnonymous()) {
256 10
                        return "unserialize({$this->exportVariable(serialize($variable))})";
257
                    }
258
259
                    return $this->exportObject($variable, $format, $level);
260 6
                } catch (Exception $e) {
261
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
262 6
                    if ($this->serializeObjects && !$reflectionClass->isAnonymous() || $reflectionClass->getName() === \stdClass::class) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->serializeObjects...e() === stdClass::class, Probably Intended Meaning: $this->serializeObjects ...() === stdClass::class)
Loading history...
263
                        try {
264 5
                            return $this->exportObject($variable, $format, $level);
265
                        } catch (Exception $e) {
266
                            return $this->exportObjectFallback($variable, $format, $level);
267
                        }
268
                    }
269
270 1
                    return $this->exportObjectFallback($variable, $format, $level);
271
                }
272
            default:
273 17
                return $this->exportVariable($variable);
274
        }
275
    }
276
277 5
    private function getPropertyName(string $property): string
278
    {
279 5
        $property = str_replace("\0", '::', trim($property));
280
281 5
        if (strpos($property, '*::') === 0) {
282
            return substr($property, 3);
283
        }
284
285 5
        if (($pos = strpos($property, '::')) !== false) {
286 4
            return substr($property, $pos + 2);
287
        }
288
289 1
        return $property;
290
    }
291
292
293
    /**
294
     * @param object $variable
295
     * @param bool $format
296
     * @param int $level
297
     * @return string
298
     * @throws ReflectionException
299
     */
300 1
    private function exportObjectFallback(object $variable, bool $format, int $level): string
301
    {
302 1
        if ($variable instanceof ArrayableInterface) {
303
            return $this->exportInternal($variable->toArray(), $format, $level);
304
        }
305
306 1
        if ($variable instanceof JsonSerializable) {
307
            return $this->exportInternal($variable->jsonSerialize(), $format, $level);
308
        }
309
310 1
        if ($variable instanceof IteratorAggregate) {
311
            return $this->exportInternal(iterator_to_array($variable), $format, $level);
312
        }
313
314
        /** @psalm-suppress RedundantCondition */
315 1
        if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
316
            return $this->exportVariable($variable->__toString());
317
        }
318
319 1
        return $this->exportVariable(self::create($variable)->asString());
320
    }
321
322 5
    private function exportObject(object $variable, bool $format, int $level): string
323
    {
324 5
        $spaces = str_repeat(' ', $level * 4);
325 5
        $objectProperties = $this->getObjectProperties($variable);
326 5
        $class = get_class($variable);
327 5
        $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(',', $this->useVarInClosures).  ')';
328
        $lines = [
329 5
            '(static function ()' . $use . ' {',
330 5
            '    $class = new \ReflectionClass(\'' . $class . '\');',
331 5
            '    $object = $class->newInstanceWithoutConstructor();',
332 5
            '    (function ()' . $use . ' {',
333
        ];
334
        $endLines = [
335 5
            '    })->bindTo($object, \'' . $class . '\')();',
336 5
            '',
337 5
            '    return $object;',
338 5
            '})()',
339
        ];
340
341
        /**
342
         * @psalm-var mixed $value
343
         * @psalm-var string $name
344
         */
345 5
        foreach ($objectProperties as $name => $value) {
346 5
            $propertyName = $this->getPropertyName($name);
347 5
            $lines[] = '        $this->' . $propertyName . ' = ' .
348 5
                $this->exportInternal($value, $format, $level + 2) . ';';
349
        }
350
351 5
        return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines));
352
    }
353
354
    /**
355
     * Exports a {@see \Closure} instance.
356
     *
357
     * @param Closure $closure Closure instance.
358
     *
359
     * @return string
360
     * @throws ReflectionException
361
     *
362
     */
363 35
    private function exportClosure(Closure $closure, int $level = 0): string
364
    {
365 35
        if (self::$closureExporter === null) {
366 1
            self::$closureExporter = new ClosureExporter();
367
        }
368
369 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

369
        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...
370
    }
371
372
    /**
373
     * @param mixed $variable
374
     *
375
     * @return string
376
     */
377 38
    private function exportVariable($variable): string
378
    {
379 38
        return var_export($variable, true);
380
    }
381
382 6
    private function getObjectDescription(object $object): string
383
    {
384 6
        return get_class($object) . '#' . spl_object_id($object);
385
    }
386
387 10
    private function getObjectProperties(object $var): array
388
    {
389 10
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
390
            /** @var array $var */
391 1
            $var = $var->__debugInfo();
392
        }
393
394 10
        return (array) $var;
395
    }
396
}
397