Dumper::buildObjectsCache()   B
last analyzed

Complexity

Conditions 9
Paths 11

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 9.0468

Importance

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