Issues (10)

src/ObjectAccess/ObjectAccess.php (2 issues)

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

281
            $data['value_type'] = $this->key(/** @scrutinizer ignore-type */ $valueType, $recursion + 1);
Loading history...
282
        }
283
        return json_encode($data);
284
    }
285
286
    /**
287
     * {@inheritDoc}
288
     */
289
    public function getValue(object $instance, string $fieldName)
290
    {
291
        $mapping = $this->getGetterMapping(new ReflectionClass($instance));
292
        if (!isset($mapping[$fieldName])) {
293
            throw new NameNotFoundException($fieldName);
294
        }
295
        $error = null;
296
        foreach ($mapping[$fieldName] as $option) {
297
            try {
298
                return $option->getValue($instance);
299
            } catch (Throwable $throwable) {
300
                $error = new ObjectAccessException($option, $fieldName, $throwable);
301
                throw $error;
302
            }
303
        }
304
        throw $error ?? new NameNotFoundException($fieldName);
305
    }
306
307
    /**
308
     * {@inheritDoc}
309
     */
310
    public function setValue(object $instance, string $fieldName, $value)
311
    {
312
        $mapping = $this->getSetterMapping(new ReflectionClass($instance));
313
        if (!isset($mapping[$fieldName])) {
314
            throw new NameNotFoundException($fieldName);
315
        }
316
        $error = null;
317
        foreach ($mapping[$fieldName] as $option) {
318
            try {
319
                return $option->setValue($instance, $value);
320
            } catch (Throwable $throwable) {
321
                $error = new ObjectWriteException($option, $fieldName, $throwable);
322
                throw $error;
323
            }
324
        }
325
        throw $error ?? new NameNotFoundException($fieldName);
326
    }
327
328
    /**
329
     * {@inheritDoc}
330
     */
331
    public function getConstructorArguments(ReflectionClass $reflectionClass): array
332
    {
333
        $constructor = $reflectionClass->getConstructor();
334
        if (!$constructor) {
335
            return [];
336
        }
337
        return $this->getMethodArguments($constructor, $reflectionClass);
338
    }
339
340
    /**
341
     * {@inheritDoc}
342
     */
343
    public function getMethodArguments(ReflectionMethod $method, ?ReflectionClass $reflectionClass = null): array
344
    {
345
        if (!$reflectionClass) {
346
            $reflectionClass = $method->getDeclaringClass();
347
        }
348
        $res = [];
349
        foreach ($method->getParameters() as $parameter) {
350
            $type = $parameter->getType();
351
            if ($type) {
352
                if ($type->isBuiltin()) {
353
                    $res[$parameter->name] = new Type($type->getName(), $type->allowsNull());
0 ignored issues
show
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

353
                    $res[$parameter->name] = new Type($type->/** @scrutinizer ignore-call */ getName(), $type->allowsNull());
Loading history...
354
                } else {
355
                    $res[$parameter->name] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->allowsNull(), $type->getName());
356
                }
357
            } else {
358
                $res[$parameter->name] = $this->guessType($reflectionClass, $parameter);
359
            }
360
        }
361
        return $res;
362
    }
363
364
365
    /**
366
     * Guess the typehint of a constructor argument if it is missing in the constructor.
367
     *
368
     * @param ReflectionClass $reflectionClass
369
     * @param ReflectionParameter $parameter
370
     * @return Type|null
371
     */
372
    private function guessType(ReflectionClass $reflectionClass, ReflectionParameter $parameter): ?Type
373
    {
374
        $types = $this->getGetterMapping($reflectionClass)[$parameter->name] ?? [];
375
        $res = [];
376
        if (empty($types)) {
377
            $types = $this->getSetterMapping($reflectionClass)[$parameter->name] ?? [];
378
            if (empty($types)) {
379
                return null;
380
            } else {
381
                $res = $this->getSetterTypes($reflectionClass, $parameter->name);
382
            }
383
        } else {
384
            $res = $this->getGetterTypes($reflectionClass, $parameter->name);
385
        }
386
        return reset($res) ? : null;
387
    }
388
389
    /**
390
     * {@inheritDoc}
391
     */
392
    public function instantiate(ReflectionClass $reflectionClass, array $constructorArgs): object
393
    {
394
        if ($this->disabledConstructor) {
395
            return $reflectionClass->newInstanceWithoutConstructor();
396
        }
397
        return $reflectionClass->newInstanceArgs($constructorArgs);
398
    }
399
}
400