Passed
Pull Request — master (#54)
by Dmitriy
07:53
created

VarDumper::exportInternal()   D

Complexity

Conditions 19
Paths 36

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 19.1925

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 19
eloc 39
c 5
b 0
f 0
nc 36
nop 3
dl 0
loc 58
ccs 34
cts 37
cp 0.9189
crap 19.1925
rs 4.5166

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

353
        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...
354
    }
355
356
    /**
357
     * @param mixed $variable
358
     *
359
     * @return string
360
     */
361 38
    private function exportVariable($variable): string
362
    {
363 38
        return var_export($variable, true);
364
    }
365
366 6
    private function getObjectDescription(object $object): string
367
    {
368 6
        return get_class($object) . '#' . spl_object_id($object);
369
    }
370
371 10
    private function getObjectProperties(object $var): array
372
    {
373 10
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
374
            /** @var array $var */
375 1
            $var = $var->__debugInfo();
376
        }
377
378 10
        return (array) $var;
379
    }
380
}
381