Passed
Push — master ( 382855...06d462 )
by Pieter
01:48
created

ObjectAccess::getConstructorArguments()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 20
rs 9.4888
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 (!$this->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 (!$this->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
     * Checks if a method's name is get.* or is.*, and can be called without parameters.
139
     */
140
    protected function isGetMethod(\ReflectionMethod $method): bool
141
    {
142
        $methodLength = \strlen($method->name);
143
144
        return
145
            !$method->isStatic() &&
146
            (
147
                ((0 === strpos($method->name, 'get') && 3 < $methodLength) ||
148
                    (0 === strpos($method->name, 'is') && 2 < $methodLength) ||
149
                    (0 === strpos($method->name, 'has') && 3 < $methodLength)) &&
150
                0 === $method->getNumberOfRequiredParameters()
151
            );
152
    }
153
154
    /**
155
     * Checks if a method's name is set.*  with 0 or 1 parameters.
156
     */
157
    protected function isSetMethod(\ReflectionMethod $method): bool
158
    {
159
        $methodLength = strlen($method->name);
160
161
        return
162
            !$method->isStatic() &&
163
            (
164
                (0 === strpos($method->name, 'set') && 3 < $methodLength)
165
                && 2 > $method->getNumberOfRequiredParameters()
166
            );
167
    }
168
169
    /**
170
     * @param ReflectionClass $reflectionClass
171
     * @param string $fieldName
172
     * @return Type[]
173
     */
174
    public function getGetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
175
    {
176
        $mapping = $this->getGetterMapping($reflectionClass);
177
        if (!isset($mapping[$fieldName])) {
178
            throw new NameNotFoundException($fieldName);
179
        }
180
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
181
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getSetterMapping');
182
    }
183
184
    /**
185
     * Returns description of a field name.
186
     *
187
     * @TODO: make a difference between getters and setters
188
     *
189
     * @param ReflectionClass $reflectionClass
190
     * @param string $fieldName
191
     * @param bool $preferGetters
192
     * @return string|null
193
     */
194
    public function getDescription(ReflectionClass $reflectionClass, string $fieldName, bool $preferGetters): ?string
195
    {
196
        return $this->phpDocExtractor->getShortDescription($reflectionClass->name, $fieldName);
197
    }
198
199
    public function getSetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
200
    {
201
        $mapping = $this->getSetterMapping($reflectionClass);
202
        if (!isset($mapping[$fieldName])) {
203
            throw new NameNotFoundException($fieldName);
204
        }
205
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
206
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getGetterMapping');
207
    }
208
209
    private function buildTypes(array $types, ReflectionClass $reflectionClass, string $fieldName, ?string $methodOnEmptyResult)
210
    {
211
        $phpDocTypes = $this->phpDocExtractor->getTypes($reflectionClass->getName(), $fieldName) ?? [];
212
        foreach ($phpDocTypes as $type) {
213
            $types[] = $type;
214
        }
215
        // fallback checking getters/setters if no typehint was given.
216
        if (empty($types) && $methodOnEmptyResult) {
217
            $mapping = $this->$methodOnEmptyResult($reflectionClass);
218
            if (!isset($mapping[$fieldName])) {
219
                return [];
220
            }
221
            $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
222
            return $this->buildTypes($res, $reflectionClass, $fieldName, null);
223
        }
224
        return $this->unique($types);
225
    }
226
227
    /**
228
     * @param Type[] $input
229
     * @return Type[]
230
     */
231
    private function unique(array $input): array
232
    {
233
        $res = [];
234
        foreach ($input as $type) {
235
            $key = $this->key($type);
236
            $res[$key] = $type;
237
238
        }
239
        return array_values($res);
240
    }
241
242
    private function key(Type $type, int $recursion = 0): string
243
    {
244
        $data = [
245
            'built_in' => $type->getBuiltinType(),
246
            'class_name' => $type->getClassName(),
247
            'collection' => $type->isCollection(),
248
            'nullable' => $type->isNullable(),
249
        ];
250
        $keyType = $type->getCollectionKeyType();
251
        if ($keyType && $recursion < 2) {
252
            $data['key_type'] = $this->key($keyType, $recursion + 1);
253
        }
254
        $valueType = $type->getCollectionValueType();
255
        if ($keyType && $recursion < 2) {
256
            $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

256
            $data['value_type'] = $this->key(/** @scrutinizer ignore-type */ $valueType, $recursion + 1);
Loading history...
257
        }
258
        return json_encode($data);
259
    }
260
261
    public function getValue(object $instance, string $fieldName)
262
    {
263
        $mapping = $this->getGetterMapping(new ReflectionClass($instance));
264
        if (!isset($mapping[$fieldName])) {
265
            throw new NameNotFoundException($fieldName);
266
        }
267
        $error = null;
268
        foreach ($mapping[$fieldName] as $option) {
269
            if ($option instanceof ReflectionMethod) {
270
                try {
271
                    return $option->invoke($instance);
272
                } catch (Throwable $throwable) {
273
                    $error = new ObjectAccessException($option, $fieldName, $throwable);
274
                }
275
            }
276
            if ($option instanceof ReflectionProperty) {
277
                if ($error) {
278
                    throw $error;
279
                }
280
                return $option->getValue($instance);
281
            }
282
        }
283
        throw $error ?? new NameNotFoundException($fieldName);
284
    }
285
286
    public function setValue(object $instance, string $fieldName, $value)
287
    {
288
        $mapping = $this->getSetterMapping(new ReflectionClass($instance));
289
        if (!isset($mapping[$fieldName])) {
290
            throw new NameNotFoundException($fieldName);
291
        }
292
        $error = null;
293
        foreach ($mapping[$fieldName] as $option) {
294
            if ($option instanceof ReflectionMethod) {
295
                try {
296
                    return $option->invoke($instance, $value);
297
                } catch (Throwable $throwable) {
298
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
299
                }
300
            }
301
            if ($option instanceof ReflectionProperty) {
302
                if ($error) {
303
                    throw $error;
304
                }
305
                try {
306
                    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...
307
                } catch (Throwable $throwable) {
308
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
309
                }
310
            }
311
        }
312
        throw $error ?? new NameNotFoundException($fieldName);
313
    }
314
315
    public function getConstructorArguments(ReflectionClass $reflectionClass): array
316
    {
317
        $constructor = $reflectionClass->getConstructor();
318
        if (!$constructor) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
319
            return [];
320
        }
321
        $res = [];
322
        foreach ($constructor->getParameters() as $parameter) {
323
            $type = $parameter->getType();
324
            if ($type) {
325
                if ($type->isBuiltin()) {
326
                    $res[$parameter->name] = new Type($type->getName(), $type->allowsNull());
327
                } else {
328
                    $res[$parameter->name] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->allowsNull(), $type->getName());
329
                }
330
            } else {
331
                $res[$parameter->name] = $this->guessType($reflectionClass, $parameter);
332
            }
333
        }
334
        return $res;
335
    }
336
337
    private function guessType(ReflectionClass $reflectionClass, ReflectionParameter $parameter): ?Type
338
    {
339
        $types = $this->getGetterMapping($reflectionClass)[$parameter->name] ?? [];
340
        $res = [];
341
        if (empty($types)) {
342
            $types = $this->getSetterMapping($reflectionClass)[$parameter->name] ?? [];
343
            if (empty($types)) {
344
                return null;
345
            } else {
346
                $res = $this->getSetterTypes($reflectionClass, $parameter->name);
347
            }
348
        } else {
349
            $res = $this->getGetterTypes($reflectionClass, $parameter->name);
350
        }
351
        return reset($res) ? : null;
352
    }
353
354
    public function instantiate(ReflectionClass $reflectionClass, array $constructorArgs): object
355
    {
356
        return $reflectionClass->newInstanceArgs($constructorArgs);
357
    }
358
}
359