ObjectAccess::setValue()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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