Passed
Push — master ( b678cc...5a9a7b )
by Pieter
03:14
created

ObjectAccess::getMethodArguments()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 19
rs 9.5222
1
<?php
2
3
namespace W2w\Lib\ApieObjectAccessNormalizer\ObjectAccess;
4
5
use ReflectionClass;
6
use ReflectionMethod;
7
use ReflectionParameter;
8
use ReflectionProperty;
9
use ReflectionType;
10
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
11
use Symfony\Component\PropertyInfo\Type;
12
use Throwable;
13
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\NameNotFoundException;
14
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\ObjectAccessException;
15
use W2w\Lib\ApieObjectAccessNormalizer\Exceptions\ObjectWriteException;
16
use W2w\Lib\ApieObjectAccessNormalizer\TypeUtils;
17
18
/**
19
 * Class that informs about object access and able to access instances of the object in setting/getting values.
20
 */
21
class ObjectAccess implements ObjectAccessInterface
22
{
23
    /**
24
     * @var (ReflectionMethod|ReflectionProperty)[][][]
25
     */
26
    private $getterCache = [];
27
28
    /**
29
     * @var (ReflectionMethod|ReflectionProperty)[][][]
30
     */
31
    private $setterCache = [];
32
33
    /**
34
     * @var PhpDocExtractor
35
     */
36
    private $phpDocExtractor;
37
38
    /**
39
     * @var int
40
     */
41
    private $methodFlags;
42
43
    /**
44
     * @var int
45
     */
46
    private $propertyFlags;
47
48
    /**
49
     * @var bool
50
     */
51
    private $disabledConstructor ;
52
53
    public function __construct(bool $publicOnly = true, bool $disabledConstructor = false)
54
    {
55
        $this->methodFlags = $publicOnly
56
            ? ReflectionMethod::IS_PUBLIC
57
            : (ReflectionMethod::IS_PUBLIC|ReflectionMethod::IS_PROTECTED|ReflectionMethod::IS_PRIVATE);
58
        $this->propertyFlags = $publicOnly
59
            ? ReflectionProperty::IS_PUBLIC
60
            : (ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE);
61
        $this->disabledConstructor = $disabledConstructor;
62
        $this->phpDocExtractor = new PhpDocExtractor();
63
    }
64
65
    /**
66
     * Sort Reflection properties and methods in order of priority which is:
67
     * - getXXX method
68
     * - isXXX method
69
     * - hasXXX method
70
     * - public property
71
     *
72
     * @param array $options
73
     */
74
    private function sort(array& $options)
75
    {
76
        usort($options, function ($a, $b) {
77
            if ($a instanceof ReflectionProperty) {
78
                return 1;
79
            }
80
            if ($b instanceof ReflectionProperty) {
81
                return -1;
82
            }
83
            /** @var ReflectionMethod $a */
84
            /** @var ReflectionMethod $b */
85
            // prio: get, is, has:
86
            if (strpos($a->getName(), 'get') === 0) {
87
                return -1;
88
            }
89
            if (strpos($b->getName(), 'get') === 0) {
90
                return 1;
91
            }
92
            if (strpos($a->getName(), 'is') === 0) {
93
                return -1;
94
            }
95
            return 1;
96
        });
97
    }
98
99
    /**
100
     * Returns all methods and properties of a class to retrieve a value.
101
     *
102
     * @param ReflectionClass $reflectionClass
103
     * @return (ReflectionMethod|ReflectionProperty)[][][]
104
     */
105
    protected function getGetterMapping(ReflectionClass $reflectionClass): array
106
    {
107
        $className= $reflectionClass->getName();
108
        if (isset($this->getterCache[$className])) {
109
            return $this->getterCache[$className];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getterCache[$className] returns the type array<mixed,array<mixed,...od|ReflectionProperty>> which is incompatible with the documented return type array<mixed,array<mixed,...d|ReflectionProperty>>>.
Loading history...
110
        }
111
112
        $attributes = [];
113
114
        $reflectionMethods = $reflectionClass->getMethods($this->methodFlags);
115
        foreach ($reflectionMethods as $method) {
116
            if (!TypeUtils::isGetMethod($method)) {
117
                continue;
118
            }
119
120
            $attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
121
            $method->setAccessible(true);
122
            $attributes[$attributeName][] = $method;
123
        }
124
        $reflectionProperties = $reflectionClass->getProperties($this->propertyFlags);
125
        foreach ($reflectionProperties as $property) {
126
            $attributeName = $property->getName();
127
            $property->setAccessible(true);
128
            $attributes[$attributeName][] = $property;
129
        }
130
        foreach ($attributes as &$methods) {
131
            $this->sort($methods);
132
        }
133
134
        return $this->getterCache[$className] = $attributes;
135
    }
136
137
    /**
138
     * Returns all methods and properties of a class that can set a value.
139
     *
140
     * @param ReflectionClass $reflectionClass
141
     * @return (ReflectionMethod|ReflectionProperty)[][][]
142
     */
143
    protected function getSetterMapping(ReflectionClass $reflectionClass): array
144
    {
145
        $className= $reflectionClass->getName();
146
        if (isset($this->setterCache[$className])) {
147
            return $this->setterCache[$className];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->setterCache[$className] returns the type array<mixed,array<mixed,...od|ReflectionProperty>> which is incompatible with the documented return type array<mixed,array<mixed,...d|ReflectionProperty>>>.
Loading history...
148
        }
149
150
        $attributes = [];
151
152
        $reflectionMethods = $reflectionClass->getMethods($this->methodFlags);
153
        foreach ($reflectionMethods as $method) {
154
            if (!TypeUtils::isSetMethod($method)) {
155
                continue;
156
            }
157
158
            $attributeName = lcfirst(substr($method->name, 3));
159
            $method->setAccessible(true);
160
            $attributes[$attributeName][] = $method;
161
        }
162
163
        $reflectionProperties = $reflectionClass->getProperties($this->propertyFlags);
164
        foreach ($reflectionProperties as $property) {
165
            $attributeName = $property->getName();
166
            $property->setAccessible(true);
167
            $attributes[$attributeName][] = $property;
168
        }
169
170
        return $this->setterCache[$className] = $attributes;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function getGetterFields(ReflectionClass $reflectionClass): array
177
    {
178
        return array_keys($this->getGetterMapping($reflectionClass));
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184
    public function getSetterFields(ReflectionClass $reflectionClass): array
185
    {
186
        return array_keys($this->getSetterMapping($reflectionClass));
187
    }
188
189
    /**
190
     * {@inheritDoc}
191
     */
192
    public function getGetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
193
    {
194
        $mapping = $this->getGetterMapping($reflectionClass);
195
        if (!isset($mapping[$fieldName])) {
196
            throw new NameNotFoundException($fieldName);
197
        }
198
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
199
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getSetterMapping');
200
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205
    public function getDescription(ReflectionClass $reflectionClass, string $fieldName, bool $preferGetters): ?string
206
    {
207
        return $this->phpDocExtractor->getShortDescription($reflectionClass->name, $fieldName);
208
    }
209
210
    /**
211
     * {@inheritDoc}
212
     */
213
    public function getSetterTypes(ReflectionClass $reflectionClass, string $fieldName): array
214
    {
215
        $mapping = $this->getSetterMapping($reflectionClass);
216
        if (!isset($mapping[$fieldName])) {
217
            throw new NameNotFoundException($fieldName);
218
        }
219
        $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
220
        return $this->buildTypes($res, $reflectionClass, $fieldName, 'getGetterMapping');
221
    }
222
223
    /**
224
     * Sanitize array of Types
225
     *
226
     * @param Type[] $types
227
     * @param ReflectionClass $reflectionClass
228
     * @param string $fieldName
229
     * @param string|null $methodOnEmptyResult
230
     * @return Type[]
231
     */
232
    private function buildTypes(array $types, ReflectionClass $reflectionClass, string $fieldName, ?string $methodOnEmptyResult)
233
    {
234
        $phpDocTypes = $this->phpDocExtractor->getTypes($reflectionClass->getName(), $fieldName) ?? [];
235
        foreach ($phpDocTypes as $type) {
236
            $types[] = $type;
237
        }
238
        // fallback checking getters/setters if no typehint was given.
239
        if (empty($types) && $methodOnEmptyResult) {
240
            $mapping = $this->$methodOnEmptyResult($reflectionClass);
241
            if (!isset($mapping[$fieldName])) {
242
                return [];
243
            }
244
            $res = TypeUtils::convertToTypeArray($mapping[$fieldName]);
245
            return $this->buildTypes($res, $reflectionClass, $fieldName, null);
246
        }
247
        return $this->unique($types);
248
    }
249
250
    /**
251
     * @param Type[] $input
252
     * @return Type[]
253
     */
254
    private function unique(array $input): array
255
    {
256
        $res = [];
257
        foreach ($input as $type) {
258
            $key = $this->key($type);
259
            $res[$key] = $type;
260
261
        }
262
        return array_values($res);
263
    }
264
265
    /**
266
     * Returns a cache key.
267
     *
268
     * @param Type $type
269
     * @param int $recursion
270
     * @return string
271
     */
272
    private function key(Type $type, int $recursion = 0): string
273
    {
274
        $data = [
275
            'built_in' => $type->getBuiltinType(),
276
            'class_name' => $type->getClassName(),
277
            'collection' => $type->isCollection(),
278
            'nullable' => $type->isNullable(),
279
        ];
280
        $keyType = $type->getCollectionKeyType();
281
        if ($keyType && $recursion < 2) {
282
            $data['key_type'] = $this->key($keyType, $recursion + 1);
283
        }
284
        $valueType = $type->getCollectionValueType();
285
        if ($keyType && $recursion < 2) {
286
            $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

286
            $data['value_type'] = $this->key(/** @scrutinizer ignore-type */ $valueType, $recursion + 1);
Loading history...
287
        }
288
        return json_encode($data);
289
    }
290
291
    /**
292
     * {@inheritDoc}
293
     */
294
    public function getValue(object $instance, string $fieldName)
295
    {
296
        $mapping = $this->getGetterMapping(new ReflectionClass($instance));
297
        if (!isset($mapping[$fieldName])) {
298
            throw new NameNotFoundException($fieldName);
299
        }
300
        $error = null;
301
        foreach ($mapping[$fieldName] as $option) {
302
            if ($option instanceof ReflectionMethod) {
303
                try {
304
                    return $option->invoke($instance);
305
                } catch (Throwable $throwable) {
306
                    $error = new ObjectAccessException($option, $fieldName, $throwable);
307
                }
308
            }
309
            if ($option instanceof ReflectionProperty) {
310
                if ($error) {
311
                    throw $error;
312
                }
313
                return $option->getValue($instance);
314
            }
315
        }
316
        throw $error ?? new NameNotFoundException($fieldName);
317
    }
318
319
    /**
320
     * {@inheritDoc}
321
     */
322
    public function setValue(object $instance, string $fieldName, $value)
323
    {
324
        $mapping = $this->getSetterMapping(new ReflectionClass($instance));
325
        if (!isset($mapping[$fieldName])) {
326
            throw new NameNotFoundException($fieldName);
327
        }
328
        $error = null;
329
        foreach ($mapping[$fieldName] as $option) {
330
            if ($option instanceof ReflectionMethod) {
331
                try {
332
                    return $option->invoke($instance, $value);
333
                } catch (Throwable $throwable) {
334
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
335
                }
336
            }
337
            if ($option instanceof ReflectionProperty) {
338
                if ($error) {
339
                    throw $error;
340
                }
341
                try {
342
                    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...
343
                } catch (Throwable $throwable) {
344
                    $error = new ObjectWriteException($option, $fieldName, $throwable);
345
                }
346
            }
347
        }
348
        throw $error ?? new NameNotFoundException($fieldName);
349
    }
350
351
    /**
352
     * {@inheritDoc}
353
     */
354
    public function getConstructorArguments(ReflectionClass $reflectionClass): array
355
    {
356
        $constructor = $reflectionClass->getConstructor();
357
        if (!$constructor) {
0 ignored issues
show
introduced by
$constructor is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
358
            return [];
359
        }
360
        return $this->getMethodArguments($constructor, $reflectionClass);
361
    }
362
363
    /**
364
     * {@inheritDoc}
365
     */
366
    public function getMethodArguments(ReflectionMethod $method, ?ReflectionClass $reflectionClass = null): array
367
    {
368
        if (!$reflectionClass) {
369
            $reflectionClass = $method->getDeclaringClass();
370
        }
371
        $res = [];
372
        foreach ($method->getParameters() as $parameter) {
373
            $type = $parameter->getType();
374
            if ($type) {
375
                if ($type->isBuiltin()) {
376
                    $res[$parameter->name] = new Type($type->getName(), $type->allowsNull());
377
                } else {
378
                    $res[$parameter->name] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->allowsNull(), $type->getName());
379
                }
380
            } else {
381
                $res[$parameter->name] = $this->guessType($reflectionClass, $parameter);
382
            }
383
        }
384
        return $res;
385
    }
386
387
388
    /**
389
     * Guess the typehint of a constructor argument if it is missing in the constructor.
390
     *
391
     * @param ReflectionClass $reflectionClass
392
     * @param ReflectionParameter $parameter
393
     * @return Type|null
394
     */
395
    private function guessType(ReflectionClass $reflectionClass, ReflectionParameter $parameter): ?Type
396
    {
397
        $types = $this->getGetterMapping($reflectionClass)[$parameter->name] ?? [];
398
        $res = [];
399
        if (empty($types)) {
400
            $types = $this->getSetterMapping($reflectionClass)[$parameter->name] ?? [];
401
            if (empty($types)) {
402
                return null;
403
            } else {
404
                $res = $this->getSetterTypes($reflectionClass, $parameter->name);
405
            }
406
        } else {
407
            $res = $this->getGetterTypes($reflectionClass, $parameter->name);
408
        }
409
        return reset($res) ? : null;
410
    }
411
412
    /**
413
     * {@inheritDoc}
414
     */
415
    public function instantiate(ReflectionClass $reflectionClass, array $constructorArgs): object
416
    {
417
        if ($this->disabledConstructor) {
418
            return $reflectionClass->newInstanceWithoutConstructor();
419
        }
420
        return $reflectionClass->newInstanceArgs($constructorArgs);
421
    }
422
}
423