Passed
Push — master ( 55f627...47fe5a )
by Dmitriy
02:36 queued 02:02
created

VarDumper::dumpInternal()   D

Complexity

Conditions 20
Paths 22

Size

Total Lines 66
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 20.3764

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 20
eloc 51
c 3
b 0
f 0
nc 22
nop 3
dl 0
loc 66
ccs 46
cts 51
cp 0.902
crap 20.3764
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 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 static 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(self::$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, self::$objects, true)) {
120 12
                return;
121
            }
122 13
            self::$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 $key => $value) {
144 5
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
145 5
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
146
                }
147
148 6
                break;
149 24
            case 'object':
150 13
                $className = get_class($var);
151
                /**
152
                 * @psalm-var array<string, array<string, array|string>> $output
153
                 */
154 13
                if (($objectCollapseLevel < $level) && (in_array($var, self::$objects, true))) {
155 11
                    if ($var instanceof \Closure) {
156 10
                        $output = $this->exportClosure($var);
157
                    } else {
158 11
                        $output = 'object@' . $this->getObjectDescription($var);
159
                    }
160 12
                } elseif ($depth <= $level) {
161 2
                    $output = $className . ' (...)';
162
                } else {
163 12
                    $output = [];
164 12
                    $mainKey = $this->getObjectDescription($var);
165 12
                    $dumpValues = $this->getObjectProperties($var);
166 12
                    if (empty($dumpValues)) {
167 1
                        $output[$mainKey] = '{stateless object}';
168
                    }
169 12
                    foreach ($dumpValues as $key => $value) {
170 11
                        $keyDisplay = $this->normalizeProperty((string)$key);
171
                        /**
172
                         * @psalm-suppress InvalidArrayOffset
173
                         */
174 11
                        $output[$mainKey][$keyDisplay] = $this->dumpNestedInternal(
175 11
                            $value,
176 11
                            $depth,
177 11
                            $level + 1,
178 11
                            $objectCollapseLevel
179
                        );
180
                    }
181
                }
182
183 13
                break;
184 13
            case 'resource':
185 1
                $output = $this->getResourceDescription($var);
186 1
                break;
187
        }
188
189 25
        return $output;
190
    }
191
192 11
    private function normalizeProperty(string $property): string
193
    {
194 11
        $property = str_replace("\0", '::', trim($property));
195
196 11
        if (strpos($property, '*::') === 0) {
197
            return 'protected::' . substr($property, 3);
198
        }
199
200 11
        if (($pos = strpos($property, '::')) !== false) {
201
            return 'private::' . substr($property, $pos + 2);
202
        }
203
204 11
        return 'public::' . $property;
205
    }
206
207
    /**
208
     * @param mixed $var variable to be dumped
209
     * @param int $depth
210
     * @param int $level depth level
211
     *
212
     * @throws \ReflectionException
213
     *
214
     * @return string
215
     */
216 25
    private function dumpInternal($var, int $depth, int $level): string
217
    {
218 25
        $type = gettype($var);
219 25
        switch ($type) {
220 25
            case 'boolean':
221 1
                return $var ? 'true' : 'false';
222 24
            case 'integer':
223 22
            case 'double':
224 6
                return (string)$var;
225 22
            case 'string':
226 6
                return "'" . addslashes($var) . "'";
227 19
            case 'resource':
228 1
                return '{resource}';
229 18
            case 'NULL':
230 1
                return 'null';
231 17
            case 'unknown type':
232
                return '{unknown}';
233 17
            case 'array':
234 4
                if ($depth <= $level) {
235
                    return '[...]';
236
                }
237
238 4
                if (empty($var)) {
239 1
                    return '[]';
240
                }
241
242 3
                $output = '';
243 3
                $keys = array_keys($var);
244 3
                $spaces = str_repeat(' ', $level * 4);
245 3
                $output .= '[';
246 3
                foreach ($keys as $name) {
247 3
                    if ($this->beautify) {
248 3
                        $output .= "\n" . $spaces . '    ';
249
                    }
250 3
                    $output .= $this->dumpInternal($name, $depth, 0);
251 3
                    $output .= ' => ';
252 3
                    $output .= $this->dumpInternal($var[$name], $depth, $level + 1);
253
                }
254
255 3
                return $this->beautify
256 3
                    ? $output . "\n" . $spaces . ']'
257 3
                    : $output . ']';
258 14
            case 'object':
259 14
                if ($var instanceof \Closure) {
260 11
                    return $this->exportClosure($var);
261
                }
262 5
                if (in_array($var, self::$objects, true)) {
263
                    return $this->getObjectDescription($var) . '(...)';
264
                }
265
266 5
                if ($depth <= $level) {
267
                    return get_class($var) . '(...)';
268
                }
269
270 5
                self::$objects[] = $var;
271 5
                $spaces = str_repeat(' ', $level * 4);
272 5
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
273 5
                $objectProperties = $this->getObjectProperties($var);
274 5
                foreach ($objectProperties as $name => $value) {
275 4
                    $propertyName = strtr(trim($name), "\0", ':');
276 4
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
277 4
                    $output .= $this->dumpInternal($value, $depth, $level + 1);
278
                }
279 5
                return $output . "\n" . $spaces . ')';
280
            default:
281
                return $type;
282
        }
283
    }
284
285 18
    private function getObjectProperties($var): array
286
    {
287 18
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
288 1
            $var = $var->__debugInfo();
289
        }
290
291 18
        return (array)$var;
292
    }
293
294 1
    private function getResourceDescription($resource)
295
    {
296 1
        $type = get_resource_type($resource);
297 1
        if ($type === 'stream') {
298 1
            $desc = stream_get_meta_data($resource);
299
        } else {
300
            $desc = '{resource}';
301
        }
302
303 1
        return $desc;
304
    }
305
306
    /**
307
     * @param mixed $variable variable to be exported
308
     * @param int $level depth level
309
     *
310
     * @throws \ReflectionException
311
     *
312
     *@return string
313
     */
314 44
    private function exportInternal($variable, int $level): string
315
    {
316 44
        switch (gettype($variable)) {
317 44
            case 'NULL':
318 2
                return 'null';
319 42
            case 'array':
320 8
                if (empty($variable)) {
321 2
                    return '[]';
322
                }
323
324 6
                $keys = array_keys($variable);
325 6
                $outputKeys = ($keys !== range(0, count($variable) - 1));
326 6
                $spaces = str_repeat(' ', $level * 4);
327 6
                $output = '[';
328 6
                foreach ($keys as $key) {
329 6
                    if ($this->beautify) {
330 3
                        $output .= "\n" . $spaces . '    ';
331
                    }
332 6
                    if ($outputKeys) {
333 2
                        $output .= $this->exportInternal($key, 0);
334 2
                        $output .= ' => ';
335
                    }
336 6
                    $output .= $this->exportInternal($variable[$key], $level + 1);
337 6
                    if ($this->beautify || next($keys) !== false) {
338 5
                        $output .= ',';
339
                    }
340
                }
341 6
                return $this->beautify
342 3
                    ? $output . "\n" . $spaces . ']'
343 6
                    : $output . ']';
344 40
            case 'object':
345 23
                if ($variable instanceof \Closure) {
346 18
                    return $this->exportClosure($variable);
347
                }
348
349
                try {
350 5
                    return 'unserialize(' . $this->exportVariable(serialize($variable)) . ')';
351 1
                } catch (\Exception $e) {
352
                    // serialize may fail, for example: if object contains a `\Closure` instance
353
                    // so we use a fallback
354 1
                    if ($variable instanceof ArrayableInterface) {
355
                        return $this->exportInternal($variable->toArray(), $level);
356
                    }
357
358 1
                    if ($variable instanceof \IteratorAggregate) {
359
                        $varAsArray = [];
360
                        foreach ($variable as $key => $value) {
361
                            $varAsArray[$key] = $value;
362
                        }
363
                        return $this->exportInternal($varAsArray, $level);
364
                    }
365
366 1
                    if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
367
                        return $this->exportVariable($variable->__toString());
368
                    }
369
370 1
                    return $this->exportVariable(self::create($variable)->asString());
371
                }
372
            default:
373 17
                return $this->exportVariable($variable);
374
        }
375
    }
376
377
    /**
378
     * Exports a [[Closure]] instance.
379
     *
380
     * @param \Closure $closure closure instance.
381
     *
382
     * @throws \ReflectionException
383
     *
384
     * @return string
385
     */
386 39
    private function exportClosure(\Closure $closure): string
387
    {
388 39
        if ($this->closureExporter === null) {
389 39
            $this->closureExporter = new ClosureExporter();
390
        }
391
392 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

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