Passed
Pull Request — master (#232)
by Alexander
05:12 queued 02:34
created

Dumper   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Test Coverage

Coverage 89.89%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 246
rs 8.64
c 2
b 0
f 0
eloc 109
ccs 80
cts 89
cp 0.8989
wmc 47

12 Methods

Rating   Name   Duplication   Size   Complexity  
A asJsonObjectsMap() 0 4 1
A __construct() 0 3 1
A getObjectProperties() 0 7 3
A create() 0 3 1
B buildObjectsCache() 0 24 9
A asJsonInternal() 0 16 2
A asJson() 0 4 1
A normalizeProperty() 0 13 3
A getObjectDescription() 0 6 2
A exportClosure() 0 3 1
D dumpNestedInternal() 0 85 19
A getResourceDescription() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like Dumper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Dumper, and based on these observations, apply Extract Interface, too.

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