Passed
Push — master ( 47fe5a...924121 )
by Alexander
02:41
created

VarDumper::dumpNestedInternal()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 60
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 12.0654

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 38
c 2
b 0
f 0
nc 9
nop 4
dl 0
loc 60
ccs 36
cts 39
cp 0.9231
crap 12.0654
rs 6.9666

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

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 19
    public function asPhpString(): string
398
    {
399 19
        $this->beautify = false;
400 19
        return $this->export();
401
    }
402
403 18
    private function getObjectDescription(object $object): string
404
    {
405 18
        return get_class($object) . '#' . spl_object_id($object);
406
    }
407
408 22
    private function exportVariable($variable): string
409
    {
410 22
        return var_export($variable, true);
411
    }
412
413 25
    private function asJsonInternal($variable, bool $prettyPrint, int $depth, int $objectCollapseLevel)
414
    {
415 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
416
417 25
        if ($prettyPrint) {
418
            $options |= JSON_PRETTY_PRINT;
419
        }
420
421 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
422
    }
423
}
424