Passed
Pull Request — master (#96)
by Dmitriy
03:14
created

Dumper::buildObjectsCache()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.0283

Importance

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

228
        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...
229
    }
230
}
231