Passed
Push — master ( 678bb1...5ccb66 )
by Alexander
02:43
created

Dumper::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug;
6
7
use Closure;
8
use Yiisoft\VarDumper\ClosureExporter;
9
10
final class Dumper
11
{
12
    /**
13
     * @var mixed Variable to dump.
14
     */
15
    private $variable;
16
17
    private array $objects = [];
18
19
    private static ?ClosureExporter $closureExporter = null;
20
21
    /**
22
     * @param mixed $variable Variable to dump.
23
     */
24 49
    private function __construct($variable)
25
    {
26 49
        $this->variable = $variable;
27 49
    }
28
29
    /**
30
     * @param mixed $variable Variable to dump.
31
     *
32
     * @return static An instance containing variable to dump.
33
     */
34 49
    public static function create($variable): self
35
    {
36 49
        return new self($variable);
37
    }
38
39
    /**
40
     * Export variable as JSON.
41
     *
42
     * @param int $depth Maximum depth that the dumper should go into the variable.
43
     * @param bool $format Whatever to format exported code.
44
     *
45
     * @return string JSON string.
46
     */
47 47
    public function asJson(int $depth = 50, bool $format = false): string
48
    {
49 47
        return $this->asJsonInternal($this->variable, $format, $depth, 0);
50
    }
51
52
    /**
53
     * Export variable as JSON summary of topmost items.
54
     *
55
     * @param int $depth Maximum depth that the dumper should go into the variable.
56
     * @param bool $prettyPrint Whatever to format exported code.
57
     *
58
     * @return string JSON string containing summary.
59
     */
60 26
    public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string
61
    {
62 26
        $this->buildObjectsCache($this->variable, $depth);
63
64 26
        return $this->asJsonInternal($this->objects, $prettyPrint, $depth, 1);
65
    }
66
67 49
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
68
    {
69 49
        if ($depth <= $level) {
70
            return;
71
        }
72 49
        if (is_object($variable)) {
73 16
            if (in_array($variable, $this->objects, true)) {
74 15
                return;
75
            }
76 16
            $this->objects[] = $variable;
77 16
            $variable = $this->getObjectProperties($variable);
78
        }
79 49
        if (is_array($variable)) {
80 40
            foreach ($variable as $value) {
81 38
                $this->buildObjectsCache($value, $depth, $level + 1);
82
            }
83
        }
84 49
    }
85
86 49
    private function asJsonInternal($variable, bool $format, int $depth, int $objectCollapseLevel)
87
    {
88 49
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
89
90 49
        if ($format) {
91
            $options |= JSON_PRETTY_PRINT;
92
        }
93
94 49
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
95
    }
96
97 49
    private function dumpNested($variable, int $depth, int $objectCollapseLevel)
98
    {
99 49
        $this->buildObjectsCache($variable, $depth);
100 49
        return $this->dumpNestedInternal($variable, $depth, 0, $objectCollapseLevel);
101
    }
102
103 16
    private function getObjectProperties($var): array
104
    {
105 16
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
106
            $var = $var->__debugInfo();
107
        }
108
109 16
        return (array)$var;
110
    }
111
112 49
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
113
    {
114 49
        $output = $var;
115
116 49
        switch (gettype($var)) {
117 49
            case 'array':
118 30
                if ($depth <= $level) {
119
                    return 'array [...]';
120
                }
121
122 30
                $output = [];
123 30
                foreach ($var as $key => $value) {
124 29
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
125 29
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
126
                }
127
128 30
                break;
129 48
            case 'object':
130 16
                $objectDescription = $this->getObjectDescription($var);
131 16
                if ($depth <= $level) {
132
                    $output = $objectDescription . ' (...)';
133
                    break;
134
                }
135
136 16
                if ($var instanceof Closure) {
137 10
                    $output = [$objectDescription => $this->exportClosure($var)];
138 10
                    break;
139
                }
140
141 7
                if ($objectCollapseLevel < $level && in_array($var, $this->objects, true)) {
142 4
                    $output = 'object@' . $objectDescription;
143 4
                    break;
144
                }
145
146 7
                $output = [];
147 7
                $properties = $this->getObjectProperties($var);
148 7
                if (empty($properties)) {
149 4
                    $output[$objectDescription] = '{stateless object}';
150 4
                    break;
151
                }
152 3
                foreach ($properties as $key => $value) {
153 3
                    $keyDisplay = $this->normalizeProperty((string)$key);
154
                    /**
155
                     * @psalm-suppress InvalidArrayOffset
156
                     */
157 3
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
158 3
                        $value,
159 3
                        $depth,
160 3
                        $level + 1,
161 3
                        $objectCollapseLevel
162
                    );
163
                }
164
165 3
                break;
166 37
            case 'resource':
167 36
            case 'resource (closed)':
168 1
                $output = $this->getResourceDescription($var);
169 1
                break;
170
        }
171
172 49
        return $output;
173
    }
174
175 16
    private function getObjectDescription(object $object): string
176
    {
177 16
        return get_class($object) . '#' . spl_object_id($object);
178
    }
179
180 3
    private function normalizeProperty(string $property): string
181
    {
182 3
        $property = str_replace("\0", '::', trim($property));
183
184 3
        if (strpos($property, '*::') === 0) {
185
            return 'protected $' . substr($property, 3);
186
        }
187
188 3
        if (($pos = strpos($property, '::')) !== false) {
189
            return 'private $' . substr($property, $pos + 2);
190
        }
191
192 3
        return 'public $' . $property;
193
    }
194
195 1
    private function getResourceDescription($resource)
196
    {
197 1
        $type = get_resource_type($resource);
198 1
        if ($type === 'stream') {
199 1
            $desc = stream_get_meta_data($resource);
200
        } else {
201
            $desc = '{resource}';
202
        }
203
204 1
        return $desc;
205
    }
206
207
    /**
208
     * Exports a {@see \Closure} instance.
209
     *
210
     * @param Closure $closure Closure instance.
211
     *
212
     * @throws \ReflectionException
213
     *
214
     * @return string
215
     */
216 10
    private function exportClosure(Closure $closure): string
217
    {
218 10
        if (self::$closureExporter === null) {
219 1
            self::$closureExporter = new ClosureExporter();
220
        }
221
222 10
        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

222
        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...
223
    }
224
}
225