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

VarDumper::asString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.1481

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 9
ccs 4
cts 6
cp 0.6667
crap 2.1481
rs 10
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