Test Failed
Pull Request — master (#34)
by Alexander
03:13 queued 33s
created

VarDumper::exportInternal()   C

Complexity

Conditions 17
Paths 27

Size

Total Lines 56
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 17.8433

Importance

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

416
        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...
417
    }
418
419
    private function getObjectDescription(object $object): string
420
    {
421
        return get_class($object) . '#' . spl_object_id($object);
422
    }
423
424
    private function exportVariable($variable): string
425
    {
426
        return var_export($variable, true);
427
    }
428
429
    private function asJsonInternal($variable, bool $format, int $depth, int $objectCollapseLevel)
430
    {
431
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
432
433
        if ($format) {
434
            $options |= JSON_PRETTY_PRINT;
435
        }
436
437
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
438
    }
439
}
440