Test Failed
Pull Request — master (#34)
by Alexander
03:07
created

VarDumper::exportInternal()   C

Complexity

Conditions 17
Paths 27

Size

Total Lines 56
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 17.9753

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

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