Passed
Push — master ( cfb4f0...a831ad )
by Alexander
02:20
created

VarDumper::exportInternal()   C

Complexity

Conditions 17
Paths 27

Size

Total Lines 56
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 17.167

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 17
eloc 37
c 3
b 0
f 0
nc 27
nop 2
dl 0
loc 56
ccs 33
cts 36
cp 0.9167
crap 17.167
rs 5.2166

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

380
        return self::$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...
381
    }
382
383 19
    public function asPhpString(): string
384
    {
385 19
        $this->beautify = false;
386 19
        return $this->export();
387
    }
388
389 18
    private function getObjectDescription(object $object): string
390
    {
391 18
        return get_class($object) . '#' . spl_object_id($object);
392
    }
393
394 33
    private function exportVariable($variable): string
395
    {
396 33
        return var_export($variable, true);
397
    }
398
399 25
    private function asJsonInternal($variable, bool $prettyPrint, int $depth, int $objectCollapseLevel)
400
    {
401 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
402
403 25
        if ($prettyPrint) {
404
            $options |= JSON_PRETTY_PRINT;
405
        }
406
407 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
408
    }
409
}
410