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

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