Passed
Pull Request — master (#22)
by Dmitriy
02:17
created

VarDumper::exportInternal()   D

Complexity

Conditions 18
Paths 28

Size

Total Lines 60
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 25.2735

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 18
eloc 40
c 2
b 0
f 0
nc 28
nop 2
dl 0
loc 60
ccs 28
cts 39
cp 0.7179
crap 25.2735
rs 4.8666

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
namespace Yiisoft\VarDumper;
4
5
use Yiisoft\Arrays\ArrayableInterface;
6
7
/**
8
 * VarDumper is intended to replace the PHP functions var_dump and print_r.
9
 * It can correctly identify the recursively referenced objects in a complex
10
 * object structure. It also has a recursive depth control to avoid indefinite
11
 * recursive display of some peculiar variables.
12
 *
13
 * VarDumper can be used as follows,
14
 *
15
 * ```php
16
 * VarDumper::dump($var);
17
 *
18
 */
19
final class VarDumper
20
{
21
    private $variable;
22
    private static array $objects = [];
23
24
    private array $exportClosureTokens = [T_FUNCTION, T_FN];
25
26
    private ?UseStatementParser $useStatementParser = null;
27
28
    private bool $beautify = true;
29
30 71
    private function __construct($variable)
31
    {
32 71
        $this->variable = $variable;
33 71
    }
34
35 71
    public static function create($variable): self
36
    {
37 71
        return new self($variable);
38
    }
39
40
    /**
41
     * Displays a variable.
42
     * This method achieves the similar functionality as var_dump and print_r
43
     * but is more robust when handling complex objects such as Yii controllers.
44
     * @param mixed $variable variable to be dumped
45
     * @param int $depth maximum depth that the dumper should go into the variable. Defaults to 10.
46
     * @param bool $highlight whether the result should be syntax-highlighted
47
     */
48
    public static function dump($variable, int $depth = 10, bool $highlight = false): void
49
    {
50
        echo self::create($variable)->asString($depth, $highlight);
51
    }
52
53
    /**
54
     * Dumps a variable in terms of a string.
55
     * This method achieves the similar functionality as var_dump and print_r
56
     * but is more robust when handling complex objects such as Yii controllers.
57
     * @param int $depth maximum depth that the dumper should go into the variable. Defaults to 10.
58
     * @param bool $highlight whether the result should be syntax-highlighted
59
     * @return string the string representation of the variable
60
     */
61 22
    public function asString(int $depth = 10, bool $highlight = false): string
62
    {
63 22
        $output = '';
64 22
        $output .= $this->dumpInternal($this->variable, $depth, 0);
65 22
        if ($highlight) {
66
            $result = highlight_string("<?php\n" . $output, true);
67
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
68
        }
69
70 22
        return $output;
71
    }
72
73 7
    private function asArray(int $depth, int $objectCollapseLevel = 0): array
74
    {
75 7
        $this->buildVarObjectsCache($this->variable, $depth);
76 7
        return $this->dumpNestedInternal($this->variable, $depth, 0, $objectCollapseLevel);
77
    }
78
79 5
    public function asJson(int $depth = 50, bool $prettyPrint = false): string
80
    {
81 5
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
82
83 5
        if ($prettyPrint) {
84
            $options |= JSON_PRETTY_PRINT;
85
        }
86
87 5
        return json_encode($this->asArray($depth), $options);
88
    }
89
90 2
    public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string
91
    {
92 2
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
93
94 2
        if ($prettyPrint) {
95
            $options |= JSON_PRETTY_PRINT;
96
        }
97
98 2
        $this->buildVarObjectsCache($this->variable, $depth);
99
100 2
        $backup = $this->variable;
101 2
        $this->variable = self::$objects;
102 2
        $output = json_encode($this->getObjectsMap($this->asArray($depth, 1)), $options);
103 2
        $this->variable = $backup;
104 2
        return $output;
105
    }
106
107
    /**
108
     * Exports a variable as a string representation.
109
     *
110
     * The string is a valid PHP expression that can be evaluated by PHP parser
111
     * and the evaluation result will give back the variable value.
112
     *
113
     * This method is similar to `var_export()`. The main difference is that
114
     * it generates more compact string representation using short array syntax.
115
     *
116
     * It also handles objects by using the PHP functions serialize() and unserialize().
117
     *
118
     * PHP 5.4 or above is required to parse the exported value.
119
     *
120
     * @return string a string representation of the variable
121
     */
122 42
    public function export(): string
123
    {
124 42
        return $this->exportInternal($this->variable, 0);
125
    }
126
127 7
    private function buildVarObjectsCache($var, int $depth, int $level = 0): void
128
    {
129 7
        if (is_array($var)) {
130 4
            if ($depth <= $level) {
131
                return;
132
            }
133 4
            foreach ($var as $key => $value) {
134 4
                $this->buildVarObjectsCache($value, $depth, $level + 1);
135
            }
136 7
        } elseif (is_object($var)) {
137 4
            if ($depth <= $level || in_array($var, self::$objects, true)) {
138 3
                return;
139
            }
140 4
            self::$objects[] = $var;
141 4
            $dumpValues = $this->getVarDumpValuesArray($var);
142 4
            foreach ($dumpValues as $key => $value) {
143 4
                $this->buildVarObjectsCache($value, $depth, $level + 1);
144
            }
145
        }
146 7
    }
147
148 7
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
149
    {
150 7
        $output = $var;
151
152 7
        switch (gettype($var)) {
153 7
            case 'array':
154 4
                if ($depth <= $level) {
155
                    return 'array [...]';
156
                }
157
158 4
                $output = [];
159 4
                foreach ($var as $key => $value) {
160 4
                    if (is_object($value)) {
161 2
                        $keyDisplay = spl_object_id($value);
162
                    } else {
163 2
                        $keyDisplay = str_replace("\0", '::', trim($key));
164
                    }
165 4
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
166
                }
167
168 4
                break;
169 7
            case 'object':
170 4
                $className = get_class($var);
171
                /**
172
                 * @psalm-var array<string, array<string, array|string>> $output
173
                 */
174 4
                if (($objectCollapseLevel < $level) && (in_array($var, self::$objects, true))) {
175 2
                    if ($var instanceof \Closure) {
176 1
                        $output = $this->exportClosure($var);
177
                    } else {
178 2
                        $output = 'object@' . $this->getObjectDescription($var);
179
                    }
180 4
                } elseif ($depth <= $level) {
181
                    $output = $className . ' (...)';
182
                } else {
183 4
                    $output = [];
184 4
                    $dumpValues = $this->getVarDumpValuesArray($var);
185 4
                    if (empty($dumpValues)) {
186
                        $output[$className] = '{stateless object}';
187
                    }
188 4
                    foreach ($dumpValues as $key => $value) {
189 4
                        $keyDisplay = $this->normalizeProperty($key);
190
                        /**
191
                         * @psalm-suppress InvalidArrayOffset
192
                         */
193 4
                        $output[$className][$keyDisplay] = $this->dumpNestedInternal(
194 4
                            $value,
195 4
                            $depth,
196 4
                            $level + 1,
197 4
                            $objectCollapseLevel
198
                        );
199
                    }
200
                }
201
202 4
                break;
203 6
            case 'resource':
204 1
                $output = $this->getResourceDescription($var);
205 1
                break;
206
        }
207
208 7
        return $output;
209
    }
210
211 4
    private function normalizeProperty(string $property): string
212
    {
213 4
        $property = str_replace("\0", '::', trim($property));
214
215 4
        if (($pos = strpos($property, '*::')) === 0) {
0 ignored issues
show
Unused Code introduced by
The assignment to $pos is dead and can be removed.
Loading history...
216
            return 'protected::' . substr($property, 3);
217
        }
218
219 4
        if (($pos = strpos($property, '::')) !== false) {
220
            return 'private::' . substr($property, $pos + 2);
221
        }
222
223 4
        return 'public::' . $property;
224
    }
225
226 2
    private function getObjectsMap(array $objectsArray): array
227
    {
228 2
        $objects = [];
229 2
        foreach ($objectsArray as $index => $object) {
230 2
            if (!is_array($object)) {
231
                continue;
232
            }
233 2
            $className = array_key_first($object);
234 2
            $objects[$className . '#' . $index] = $object[$className];
235
        }
236 2
        return $objects;
237
    }
238
239
    /**
240
     * @param mixed $var variable to be dumped
241
     * @param int $level depth level
242
     * @return string
243
     */
244 22
    private function dumpInternal($var, int $depth, int $level): string
245
    {
246 22
        $type = gettype($var);
247 22
        switch ($type) {
248 22
            case 'boolean':
249 1
                return $var ? 'true' : 'false';
250 21
            case 'integer':
251 19
            case 'double':
252 6
                return (string)$var;
253 19
            case 'string':
254 5
                return "'" . addslashes($var) . "'";
255 17
            case 'resource':
256 1
                return '{resource}';
257 16
            case 'NULL':
258 1
                return 'null';
259 15
            case 'unknown type':
260
                return '{unknown}';
261 15
            case 'array':
262 4
                if ($depth <= $level) {
263
                    return '[...]';
264
                }
265
266 4
                if (empty($var)) {
267 1
                    return '[]';
268
                }
269
270 3
                $output = '';
271 3
                $keys = array_keys($var);
272 3
                $spaces = str_repeat(' ', $level * 4);
273 3
                $output .= '[';
274 3
                foreach ($keys as $key) {
275 3
                    if ($this->beautify) {
276 3
                        $output .= "\n" . $spaces . '    ';
277
                    }
278 3
                    $output .= $this->dumpInternal($key, $depth, 0);
279 3
                    $output .= ' => ';
280 3
                    $output .= $this->dumpInternal($var[$key], $depth, $level + 1);
281
                }
282
283 3
                return $this->beautify
284 3
                    ? $output . "\n" . $spaces . ']'
285 3
                    : $output . ']';
286 12
            case 'object':
287 12
                if ($var instanceof \Closure) {
288 9
                    return $this->exportClosure($var);
289
                }
290 3
                if (in_array($var, self::$objects, true)) {
291
                    return $this->getObjectDescription($var) . '(...)';
292
                }
293
294 3
                if ($depth <= $level) {
295
                    return get_class($var) . '(...)';
296
                }
297
298 3
                self::$objects[] = $var;
299 3
                $spaces = str_repeat(' ', $level * 4);
300 3
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
301 3
                $dumpValues = $this->getVarDumpValuesArray($var);
302 3
                foreach ($dumpValues as $key => $value) {
303 2
                    $keyDisplay = strtr(trim($key), "\0", ':');
304 2
                    $output .= "\n" . $spaces . "    [$keyDisplay] => ";
305 2
                    $output .= $this->dumpInternal($value, $depth, $level + 1);
306
                }
307 3
                return $output . "\n" . $spaces . ')';
308
            default:
309
                return $type;
310
        }
311
    }
312
313 7
    private function getVarDumpValuesArray($var): array
314
    {
315 7
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
316 1
            $dumpValues = $var->__debugInfo();
317 1
            if (!is_array($dumpValues)) {
318
                throw new \Exception('__debugInfo() must return an array');
319
            }
320 1
            return $dumpValues;
321
        }
322
323 6
        return (array)$var;
324
    }
325
326 1
    private function getResourceDescription($resource)
327
    {
328 1
        $type = get_resource_type($resource);
329 1
        if ($type === 'stream') {
330 1
            $desc = stream_get_meta_data($resource);
331
        } else {
332
            $desc = '{resource}';
333
        }
334
335 1
        return $desc;
336
    }
337
338
    /**
339
     * @param mixed $var variable to be exported
340
     * @param int $level depth level
341
     * @return string
342
     * @throws \ReflectionException
343
     */
344 42
    private function exportInternal($var, int $level): string
345
    {
346 42
        switch (gettype($var)) {
347 42
            case 'NULL':
348 2
                return 'null';
349 40
            case 'array':
350 8
                if (empty($var)) {
351 2
                    return '[]';
352
                }
353
354 6
                $keys = array_keys($var);
355 6
                $outputKeys = ($keys !== range(0, count($var) - 1));
356 6
                $spaces = str_repeat(' ', $level * 4);
357 6
                $output = '[';
358 6
                foreach ($keys as $key) {
359 6
                    if ($this->beautify) {
360 3
                        $output .= "\n" . $spaces . '    ';
361
                    }
362 6
                    if ($outputKeys) {
363 2
                        $output .= $this->exportInternal($key, 0);
364 2
                        $output .= ' => ';
365
                    }
366 6
                    $output .= $this->exportInternal($var[$key], $level + 1);
367 6
                    if ($this->beautify || next($keys) !== false) {
368 5
                        $output .= ',';
369
                    }
370
                }
371 6
                return $this->beautify
372 3
                    ? $output . "\n" . $spaces . ']'
373 6
                    : $output . ']';
374 38
            case 'object':
375 22
                if ($var instanceof \Closure) {
376 18
                    return $this->exportClosure($var);
377
                }
378
379
                try {
380 4
                    return 'unserialize(' . var_export(serialize($var), true) . ')';
381
                } catch (\Exception $e) {
382
                    // serialize may fail, for example: if object contains a `\Closure` instance
383
                    // so we use a fallback
384
                    if ($var instanceof ArrayableInterface) {
385
                        return $this->exportInternal($var->toArray(), $level);
386
                    }
387
388
                    if ($var instanceof \IteratorAggregate) {
389
                        $varAsArray = [];
390
                        foreach ($var as $key => $value) {
391
                            $varAsArray[$key] = $value;
392
                        }
393
                        return $this->exportInternal($varAsArray, $level);
394
                    }
395
396
                    if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__toString')) {
397
                        return var_export($var->__toString(), true);
398
                    }
399
400
                    return var_export(self::create($var)->asString(), true);
401
                }
402
            default:
403 16
                return var_export($var, true);
404
        }
405
    }
406
407
    /**
408
     * Exports a [[Closure]] instance.
409
     * @param \Closure $closure closure instance.
410
     * @return string
411
     * @throws \ReflectionException
412
     */
413 28
    private function exportClosure(\Closure $closure): string
414
    {
415 28
        $reflection = new \ReflectionFunction($closure);
416
417 28
        $fileName = $reflection->getFileName();
418 28
        $start = $reflection->getStartLine();
419 28
        $end = $reflection->getEndLine();
420
421 28
        if ($fileName === false || $start === false || $end === false) {
422
            return 'function() {/* Error: unable to determine Closure source */}';
423
        }
424
425 28
        --$start;
426 28
        $uses = $this->getUsesParser()->fromFile($fileName);
427
428 28
        $source = implode('', array_slice(file($fileName), $start, $end - $start));
0 ignored issues
show
Bug introduced by
It seems like file($fileName) can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

428
        $source = implode('', array_slice(/** @scrutinizer ignore-type */ file($fileName), $start, $end - $start));
Loading history...
429 28
        $tokens = token_get_all('<?php ' . $source);
430 28
        array_shift($tokens);
431
432 28
        $closureTokens = [];
433 28
        $pendingParenthesisCount = 0;
434 28
        $isShortClosure = false;
435 28
        $buffer = '';
436 28
        foreach ($tokens as $token) {
437 28
            if (!isset($token[0])) {
438
                continue;
439
            }
440 28
            if (in_array($token[0], $this->exportClosureTokens, true)) {
441 28
                $closureTokens[] = $token[1];
442 28
                if (!$isShortClosure && $token[0] === T_FN) {
443 22
                    $isShortClosure = true;
444
                }
445 28
                continue;
446
            }
447 28
            if ($closureTokens !== []) {
448 28
                $readableToken = $token[1] ?? $token;
449 28
                if ($this->isNextTokenIsPartOfNamespace($token)) {
450 15
                    $buffer .= $token[1];
451 15
                    if (!$this->isNextTokenIsPartOfNamespace(next($tokens)) && array_key_exists($buffer, $uses)) {
452 9
                        $readableToken = $uses[$buffer];
453 9
                        $buffer = '';
454
                    }
455
                }
456 28
                if ($token === '{' || $token === '[') {
457 9
                    $pendingParenthesisCount++;
458 28
                } elseif ($token === '}' || $token === ']') {
459 12
                    if ($pendingParenthesisCount === 0) {
460 3
                        break;
461
                    }
462 9
                    $pendingParenthesisCount--;
463 28
                } elseif ($token === ',' || $token === ';') {
464 25
                    if ($pendingParenthesisCount === 0) {
465 25
                        break;
466
                    }
467
                }
468 28
                $closureTokens[] = $readableToken;
469
            }
470
        }
471
472 28
        return implode('', $closureTokens);
473
    }
474
475 19
    public function asPhpString(): string
476
    {
477 19
        $this->exportClosureTokens = [T_FUNCTION, T_FN, T_STATIC];
478 19
        $this->beautify = false;
479 19
        return $this->export();
480
    }
481
482 28
    private function getUsesParser(): UseStatementParser
483
    {
484 28
        if ($this->useStatementParser === null) {
485 28
            $this->useStatementParser = new UseStatementParser();
486
        }
487
488 28
        return $this->useStatementParser;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->useStatementParser could return the type null which is incompatible with the type-hinted return Yiisoft\VarDumper\UseStatementParser. Consider adding an additional type-check to rule them out.
Loading history...
489
    }
490
491 28
    private function isNextTokenIsPartOfNamespace($token): bool
492
    {
493 28
        if (!is_array($token)) {
494 28
            return false;
495
        }
496
497 28
        return $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR;
498
    }
499
500 4
    private function getObjectDescription(object $object): string
501
    {
502 4
        return get_class($object) . '#' . spl_object_id($object);
503
    }
504
}
505