Passed
Push — master ( 06d462...966295 )
by Pieter
01:36
created

ObjectAccess   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 160
c 1
b 0
f 0
dl 0
loc 308
rs 3.2
wmc 65

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getSetterMapping() 0 28 5
A __construct() 0 9 3
A sort() 0 22 6
A getGetterFields() 0 3 1
A getSetterFields() 0 3 1
B getGetterMapping() 0 30 7
A instantiate() 0 3 1
A getDescription() 0 3 1
A getConstructorArguments() 0 20 5
B getValue() 0 23 7
A key() 0 17 5
B setValue() 0 27 8
A buildTypes() 0 16 5
A getSetterTypes() 0 8 2
A unique() 0 9 2
A getGetterTypes() 0 8 2
A guessType() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like ObjectAccess 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 ObjectAccess, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace W2w\Lib\ApieObjectAccessNormalizer\ObjectAccess;
4
5
use ReflectionClass;
6
use ReflectionMethod;
7
use ReflectionParameter;
8
use ReflectionProperty;
9
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
10
use Symfony\Component\PropertyInfo\Type;
11
use Throwable;
12
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\NameNotFoundException;
13
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\ObjectAccessException;
14
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\ObjectWriteException;
15
use W2w\Lib\ApieObjectAccessNormalizer\TypeUtils;
16
17
class ObjectAccess implements ObjectAccessInterface
18
{
19
    private $getterCache = [];
20
21
    private $setterCache = [];
22
23
    private $phpDocExtractor;
24
25
    private $methodFlags;
26
27
    private $propertyFlags;
28
29
    public function __construct(bool $publicOnly = true)
30
    {
31
        $this->methodFlags = $publicOnly
32
            ? ReflectionMethod::IS_PUBLIC
33
            : (ReflectionMethod::IS_PUBLIC|ReflectionMethod::IS_PROTECTED|ReflectionMethod::IS_PRIVATE);
34
        $this->propertyFlags = $publicOnly
35
            ? ReflectionProperty::IS_PUBLIC
36
            : (ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE);
37
        $this->phpDocExtractor = new PhpDocExtractor();
38
    }
39
40
    private function sort(array& $options)
41
    {
42
        usort($options, function ($a, $b) {
43
            if ($a instanceof ReflectionProperty) {
44
                return 1;
45
            }
46
            if ($b instanceof ReflectionProperty) {
47
                return -1;
48
            }
49
            /** @var ReflectionMethod $a */
50
            /** @var ReflectionMethod $b */
51
            // prio: get, is, has:
52
            if (strpos($a->getName(), 'get') === 0) {
53
                return -1;
54
            }
55
            if (strpos($b->getName(), 'get') === 0) {
56
                return 1;
57
            }
58
            if (strpos($a->getName(), 'is') === 0) {
59
                return -1;
60
            }
61
            return 1;
62
        });
63
    }
64
65
    protected function getGetterMapping(ReflectionClass $reflectionClass): array
66
    {
67
        $className= $reflectionClass->getName();
68
        if (isset($this->getterCache[$className])) {
69
            return $this->getterCache[$className];
70
        }
71
72
        $attributes = [];
73
74
        $reflectionMethods = $reflectionClass->getMethods($this->methodFlags);
75
        foreach ($reflectionMethods as $method) {
76
            if (!TypeUtils::isGetMethod($method)) {
77
                continue;
78
            }
79
80
            $attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
81
            $method->setAccessible(true);
82
            $attributes[$attributeName][] = $method;
83
        }
84
        $reflectionProperties = $reflectionClass->getProperties($this->propertyFlags);
85
        foreach ($reflectionProperties as $property) {
86
            $attributeName = $property->getName();
87
            $property->setAccessible(true);
88
            $attributes[$attributeName][] = $property;
89
        }
90
        foreach ($attributes as &$methods) {
91
            $this->sort($methods);
92
        }
93
94
        return $this->getterCache[$className] = $attributes;
95
    }
96
97
    protected function getSetterMapping(ReflectionClass $reflectionClass): array
98
    {
99
        $className= $reflectionClass->getName();
100
        if (isset($this->setterCache[$className])) {
101
            return $this->setterCache[$className];
102
        }
103
104
        $attributes = [];
105
106
        $reflectionMethods = $reflectionClass->getMethods($this->methodFlags);
107
        foreach ($reflectionMethods as $method) {
108
            if (!TypeUtils::isSetMethod($method)) {
109
                continue;
110
            }
111
112
            $attributeName = lcfirst(substr($method->name, 3));
113
            $method->setAccessible(true);
114
            $attributes[$attributeName][] = $method;
115
        }
116
117
        $reflectionProperties = $reflectionClass->getProperties($this->propertyFlags);
118
        foreach ($reflectionProperties as $property) {
119
            $attributeName = $property->getName();
120
            $property->setAccessible(true);
121
            $attributes[$attributeName][] = $property;
122
        }
123
124
        return $this->setterCache[$className] = $attributes;
125
    }
126
127
    public function getGetterFields(ReflectionClass $reflectionClass): array
128
    {
129
        return array_keys($this->getGetterMapping($reflectionClass));
130
    }
131
132
    public function getSetterFields(ReflectionClass $reflectionClass): array
133
    {
134
        return array_keys($this->getSetterMapping($reflectionClass));
135
    }
136
137
    /**
138
     * @param ReflectionClass $reflectionClass
139
     * @param string $fieldName
140
     * @return Type[]
141
     */
142
    public function getGetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
143
    {
144
        $mapping = $this->getGetterMapping($reflectionClass);
145
        if (!isset($mapping[$fieldName])) {
146
            throw new NameNotFoundException($fieldName);
147
        }
148
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
149
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getSetterMapping');
150
    }
151
152
    /**
153
     * Returns description of a field name.
154
     *
155
     * @TODO: make a difference between getters and setters
156
     *
157
     * @param ReflectionClass $reflectionClass
158
     * @param string $fieldName
159
     * @param bool $preferGetters
160
     * @return string|null
161
     */
162
    public function getDescription(ReflectionClass $reflectionClass, string $fieldName, bool $preferGetters): ?string
163
    {
164
        return $this->phpDocExtractor->getShortDescription($reflectionClass->name, $fieldName);
165
    }
166
167
    public function getSetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
168
    {
169
        $mapping = $this->getSetterMapping($reflectionClass);
170
        if (!isset($mapping[$fieldName])) {
171
            throw new NameNotFoundException($fieldName);
172
        }
173
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
174
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getGetterMapping');
175
    }
176
177
    private function buildTypes(array $types, ReflectionClass $reflectionClass, string $fieldName, ?string $methodOnEmptyResult)
178
    {
179
        $phpDocTypes = $this->phpDocExtractor->getTypes($reflectionClass->getName(), $fieldName) ?? [];
180
        foreach ($phpDocTypes as $type) {
181
            $types[] = $type;
182
        }
183
        // fallback checking getters/setters if no typehint was given.
184
        if (empty($types) && $methodOnEmptyResult) {
185
            $mapping = $this->$methodOnEmptyResult($reflectionClass);
186
            if (!isset($mapping[$fieldName])) {
187
                return [];
188
            }
189
            $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
190
            return $this->buildTypes($res, $reflectionClass, $fieldName, null);
191
        }
192
        return $this->unique($types);
193
    }
194
195
    /**
196
     * @param Type[] $input
197
     * @return Type[]
198
     */
199
    private function unique(array $input): array
200
    {
201
        $res = [];
202
        foreach ($input as $type) {
203
            $key = $this->key($type);
204
            $res[$key] = $type;
205
206
        }
207
        return array_values($res);
208
    }
209
210
    private function key(Type $type, int $recursion = 0): string
211
    {
212
        $data = [
213
            'built_in' => $type->getBuiltinType(),
214
            'class_name' => $type->getClassName(),
215
            'collection' => $type->isCollection(),
216
            'nullable' => $type->isNullable(),
217
        ];
218
        $keyType = $type->getCollectionKeyType();
219
        if ($keyType && $recursion < 2) {
220
            $data['key_type'] = $this->key($keyType, $recursion + 1);
221
        }
222
        $valueType = $type->getCollectionValueType();
223
        if ($keyType && $recursion < 2) {
224
            $data['value_type'] = $this->key($valueType, $recursion + 1);
0 ignored issues
show
Bug introduced by
It seems like $valueType can also be of type null; however, parameter $type of W2w\Lib\ApieObjectAccess...ess\ObjectAccess::key() does only seem to accept Symfony\Component\PropertyInfo\Type, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
            $data['value_type'] = $this->key(/** @scrutinizer ignore-type */ $valueType, $recursion + 1);
Loading history...
225
        }
226
        return json_encode($data);
227
    }
228
229
    public function getValue(object $instance, string $fieldName)
230
    {
231
        $mapping = $this->getGetterMapping(new ReflectionClass($instance));
232
        if (!isset($mapping[$fieldName])) {
233
            throw new NameNotFoundException($fieldName);
234
        }
235
        $error = null;
236
        foreach ($mapping[$fieldName] as $option) {
237
            if ($option instanceof ReflectionMethod) {
238
                try {
239
                    return $option->invoke($instance);
240
                } catch (Throwable $throwable) {
241
                    $error = new ObjectAccessException($option, $fieldName, $throwable);
242
                }
243
            }
244
            if ($option instanceof ReflectionProperty) {
245
                if ($error) {
246
                    throw $error;
247
                }
248
                return $option->getValue($instance);
249
            }
250
        }
251
        throw $error ?? new NameNotFoundException($fieldName);
252
    }
253
254
    public function setValue(object $instance, string $fieldName, $value)
255
    {
256
        $mapping = $this->getSetterMapping(new ReflectionClass($instance));
257
        if (!isset($mapping[$fieldName])) {
258
            throw new NameNotFoundException($fieldName);
259
        }
260
        $error = null;
261
        foreach ($mapping[$fieldName] as $option) {
262
            if ($option instanceof ReflectionMethod) {
263
                try {
264
                    return $option->invoke($instance, $value);
265
                } catch (Throwable $throwable) {
266
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
267
                }
268
            }
269
            if ($option instanceof ReflectionProperty) {
270
                if ($error) {
271
                    throw $error;
272
                }
273
                try {
274
                    return $option->setValue($instance, $value);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $option->setValue($instance, $value) targeting ReflectionProperty::setValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
275
                } catch (Throwable $throwable) {
276
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
277
                }
278
            }
279
        }
280
        throw $error ?? new NameNotFoundException($fieldName);
281
    }
282
283
    public function getConstructorArguments(ReflectionClass $reflectionClass): array
284
    {
285
        $constructor = $reflectionClass->getConstructor();
286
        if (!$constructor) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
287
            return [];
288
        }
289
        $res = [];
290
        foreach ($constructor->getParameters() as $parameter) {
291
            $type = $parameter->getType();
292
            if ($type) {
293
                if ($type->isBuiltin()) {
294
                    $res[$parameter->name] = new Type($type->getName(), $type->allowsNull());
295
                } else {
296
                    $res[$parameter->name] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->allowsNull(), $type->getName());
297
                }
298
            } else {
299
                $res[$parameter->name] = $this->guessType($reflectionClass, $parameter);
300
            }
301
        }
302
        return $res;
303
    }
304
305
    private function guessType(ReflectionClass $reflectionClass, ReflectionParameter $parameter): ?Type
306
    {
307
        $types = $this->getGetterMapping($reflectionClass)[$parameter->name] ?? [];
308
        $res = [];
309
        if (empty($types)) {
310
            $types = $this->getSetterMapping($reflectionClass)[$parameter->name] ?? [];
311
            if (empty($types)) {
312
                return null;
313
            } else {
314
                $res = $this->getSetterTypes($reflectionClass, $parameter->name);
315
            }
316
        } else {
317
            $res = $this->getGetterTypes($reflectionClass, $parameter->name);
318
        }
319
        return reset($res) ? : null;
320
    }
321
322
    public function instantiate(ReflectionClass $reflectionClass, array $constructorArgs): object
323
    {
324
        return $reflectionClass->newInstanceArgs($constructorArgs);
325
    }
326
}
327