Passed
Push — master ( 221c35...fdb910 )
by Petr
02:59
created

Processing::getOptionAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace kalanis\Pohoda\Common\Dtos;
4
5
use ReflectionAttribute;
6
use ReflectionClass;
7
use ReflectionException;
8
use ReflectionNamedType;
9
use ReflectionProperty;
10
use ReflectionUnionType;
11
use kalanis\Pohoda\Common\Attributes;
12
13
class Processing
14
{
15
    /**
16
     * Throw out the entries that cannot be used for check - containing null or empty array
17
     *
18
     * @param array<string, mixed> $data
19
     * @return array<string, mixed>
20
     */
21 152
    public static function filterUnusableData(array $data): array
22
    {
23
        // throw nulls and empty arrays (unused values) out
24 152
        return array_filter(
25 152
            $data,
26 152
            fn($in) => !(is_null($in) || (is_array($in) && empty($in))),
27 152
        );
28
    }
29
30
    /**
31
     * Get all options as set in classes set for available attributes
32
     *
33
     * @param AbstractDto $class
34
     * @param bool $responseDirection
35
     *
36
     * @return array<string, object[]>
37
     */
38 169
    public static function getOptions(AbstractDto $class, bool $responseDirection): array
39
    {
40 169
        $reflection = new ReflectionClass($class);
41 169
        $allProperties = $reflection->getProperties();
42 169
        $ref = [];
43 169
        foreach ($allProperties as $property) {
44 167
            if (static::hasInternalAttribute($property)) {
45 87
                continue;
46
            }
47 167
            if (static::hasDirectionAttribute($property) && !$responseDirection) {
48 35
                continue;
49
            }
50 167
            $options = static::getOptionAttributes($property);
51 167
            foreach ($options as $option) {
52 167
                $propName = $property->getName();
53 167
                if (!isset($ref[$propName])) {
54 167
                    $ref[$propName] = [];
55
                }
56 167
                $ref[$propName][] = $option->newInstance();
57
            }
58
        }
59 169
        return $ref;
60
    }
61
62
    /**
63
     * Get all referenced attributes from DTOs
64
     *
65
     * @param AbstractDto $class
66
     * @param bool $responseDirection
67
     *
68
     * @return string[]
69
     */
70 119
    public static function getRefAttributes(AbstractDto $class, bool $responseDirection): array
71
    {
72 119
        $reflection = new ReflectionClass($class);
73 119
        $allProperties = $reflection->getProperties();
74 119
        $ref = [];
75 119
        foreach ($allProperties as $property) {
76 119
            if (static::hasInternalAttribute($property)) {
77 55
                continue;
78
            }
79 119
            if (static::hasDirectionAttribute($property) && !$responseDirection) {
80 24
                continue;
81
            }
82 119
            if (static::hasRefAttribute($property)) {
83 85
                $ref[] = $property->getName();
84
            }
85
        }
86 119
        return $ref;
87
    }
88
89
    /**
90
     * Get all attributes from DTOs which are just attributes to different elements in target XML
91
     *
92
     * @param AbstractDto $class
93
     * @param bool $responseDirection
94
     *
95
     * @return array<string, Attributes\AttributeExtend>
96
     */
97 119
    public static function getAttributesExtendingElements(AbstractDto $class, bool $responseDirection): array
98
    {
99 119
        $reflection = new ReflectionClass($class);
100 119
        $allProperties = $reflection->getProperties();
101 119
        $ref = [];
102 119
        foreach ($allProperties as $property) {
103 119
            if (static::hasInternalAttribute($property)) {
104 53
                continue;
105
            }
106 119
            if (static::isJustAttribute($property)) {
107 31
                continue;
108
            }
109 119
            if (static::hasDirectionAttribute($property) && !$responseDirection) {
110 24
                continue;
111
            }
112 119
            if ($extend = static::getExtendingAttribute($property)) {
113 70
                $ref[$property->getName()] = $extend;
114
            }
115
        }
116 119
        return $ref;
117
    }
118
119
    /**
120
     * Get all properties of DTO
121
     *
122
     * @param AbstractDto $class
123
     * @param bool $withAttributes
124
     * @param bool $responseDirection
125
     *
126
     * @return string[]
127
     */
128 171
    public static function getProperties(AbstractDto $class, bool $withAttributes, bool $responseDirection): array
129
    {
130 171
        $reflection = new ReflectionClass($class);
131 171
        $props = [];
132 171
        foreach ($reflection->getProperties() as $prop) {
133 169
            if (static::hasInternalAttribute($prop)) {
134 89
                continue;
135
            }
136 169
            if (static::hasDirectionAttribute($prop) && !$responseDirection) {
137 36
                continue;
138
            }
139 169
            if (static::isJustAttribute($prop) && !$withAttributes) {
140 17
                continue;
141
            }
142 169
            $props[] = $prop->getName();
143
        }
144 171
        return $props;
145
    }
146
147
    /**
148
     * Fill DTO with data, change types when necessary
149
     *
150
     * @param AbstractDto $class
151
     * @param array<string, mixed> $data
152
     * @param bool $responseDirection
153
     *
154
     * @throws ReflectionException
155
     *
156
     * @return AbstractDto
157
     */
158 155
    public static function hydrate(AbstractDto $class, array $data, bool $responseDirection): AbstractDto
159
    {
160 155
        $reflection = new ReflectionClass($class);
161 155
        $clonedInstance = $reflection->newInstance();
162 155
        $usedKeys = [];
163
        // regular defined properties
164 155
        $allProperties = $reflection->getProperties();
165 155
        foreach ($allProperties as $property) {
166 153
            $usedKeys[] = $property->getName();
167 153
            if (static::hasInternalAttribute($property)) {
168 76
                continue;
169
            }
170 153
            if (static::hasDirectionAttribute($property) && !$responseDirection) {
171 30
                continue;
172
            }
173
174 153
            if (isset($data[$property->getName()])) {
175 152
                $value = $data[$property->getName()];
176 152
                $propertyType = static::getPropertyType($property, $value);
177
178 152
                if (!empty($propertyType)) {
179
                    // need to know what type will be used; there can be multiple targets, so it need to behave correctly
180 152
                    foreach (static::getRepresentsAttributes($property) as $key) {
181 152
                        static::hydrateClonedInstance($clonedInstance, $key, $propertyType, $value, $property);
182
                    }
183
                }
184
            }
185
        }
186
187
        // now dynamically added ones
188 155
        static::hydrateDynamicallySetProperties($clonedInstance, $class, $usedKeys, $data);
189 155
        return $clonedInstance;
190
    }
191
192
    /**
193
     * Check if this property shall be skipped
194
     *
195
     * @param ReflectionProperty $property
196
     *
197
     * @return bool
198
     */
199 179
    protected static function hasInternalAttribute(ReflectionProperty $property): bool
200
    {
201 179
        return !empty($property->getAttributes(Attributes\OnlyInternal::class));
202
    }
203
204
    /**
205
     * Check if the property is used as reference to somewhere
206
     *
207
     * @param ReflectionProperty $property
208
     *
209
     * @return bool
210
     */
211 119
    protected static function hasRefAttribute(ReflectionProperty $property): bool
212
    {
213 119
        return !empty($property->getAttributes(Attributes\RefElement::class));
214
    }
215
216
    /**
217
     * Check if the property is used only in direction for response
218
     *
219
     * @param ReflectionProperty $property
220
     *
221
     * @return bool
222
     */
223 179
    protected static function hasDirectionAttribute(ReflectionProperty $property): bool
224
    {
225 179
        return !empty($property->getAttributes(Attributes\ResponseDirection::class));
226
    }
227
228
    /**
229
     * Check if the property is used only as attribute
230
     *
231
     * @param ReflectionProperty $property
232
     *
233
     * @return bool
234
     */
235 171
    protected static function isJustAttribute(ReflectionProperty $property): bool
236
    {
237 171
        return !empty($property->getAttributes(Attributes\JustAttribute::class));
238
    }
239
240
    /**
241
     * Use different attribute as target instead of the one now processed
242
     *
243
     * @param ReflectionProperty $property
244
     *
245
     * @return iterable<string>
246
     */
247 152
    protected static function getRepresentsAttributes(ReflectionProperty $property): iterable
248
    {
249 152
        $attrs = $property->getAttributes(Attributes\Represents::class);
250 152
        $anyDefined = false;
251 152
        foreach ($attrs as $attr) {
252 3
            $instance = $attr->newInstance();
253 3
            if (\is_a($instance, Attributes\Represents::class)) {
254 3
                foreach ((array) $instance->differentVariable as $variable) {
255 3
                    $anyDefined = true;
256 3
                    yield $variable;
257
                }
258
            }
259
        }
260 152
        if (!$anyDefined) {
261 151
            yield $property->getName();
262
        }
263
    }
264
265
    /**
266
     * Use different attribute as target in XML instead that one used
267
     *
268
     * @param ReflectionProperty $property
269
     *
270
     * @return null|Attributes\AttributeExtend
271
     */
272 119
    protected static function getExtendingAttribute(ReflectionProperty $property): ?Attributes\AttributeExtend
273
    {
274 119
        $attrs = $property->getAttributes(Attributes\AttributeExtend::class);
275 119
        foreach ($attrs as $attr) {
276 70
            $instance = $attr->newInstance();
277 70
            if (\is_a($instance, Attributes\AttributeExtend::class)) {
278 70
                return $instance;
279
            }
280
        }
281 119
        return null;
282
    }
283
284
    /**
285
     * Use different attribute as target instead of the one now processed
286
     *
287
     * @param ReflectionProperty $property
288
     *
289
     * @return ReflectionAttribute<object>[]
290
     */
291 167
    protected static function getOptionAttributes(ReflectionProperty $property): array
292
    {
293 167
        return $property->getAttributes(Attributes\Options\AbstractOption::class, ReflectionAttribute::IS_INSTANCEOF);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $property->getAtt...tribute::IS_INSTANCEOF) returns the type ReflectionAttribute[] which is incompatible with the documented return type ReflectionAttribute.
Loading history...
294
    }
295
296
    /**
297
     * @param ReflectionProperty $property
298
     * @param mixed $value
299
     *
300
     * @throws ReflectionException
301
     *
302
     * @return string|null
303
     */
304 152
    protected static function getPropertyType(ReflectionProperty $property, mixed $value): ?string
305
    {
306 152
        $type = $property->getType();
307 152
        if (empty($type)) {
308
            // @codeCoverageIgnoreStart
309
            // cannot find the case, but the definition says there is at least one
310
            return null;
311
        }
312
        // @codeCoverageIgnoreEnd
313 152
        if (\is_a($type, ReflectionUnionType::class)) {
314 127
            return static::getPropertyTypeFromUnion($type, $value);
315
        }
316 148
        if (\method_exists($type, 'getName')) {
317 148
            return $type->getName();
318
        }
319
        // @codeCoverageIgnoreStart
320
        // last fallback when everything else fails
321
        return \get_class($type);
322
        // @codeCoverageIgnoreEnd
323
    }
324
325
    /**
326
     * @param ReflectionUnionType $unionType
327
     * @param mixed $value
328
     *
329
     * @throws ReflectionException
330
     *
331
     * @return string|null
332
     */
333 127
    protected static function getPropertyTypeFromUnion(ReflectionUnionType $unionType, mixed $value): ?string
334
    {
335 127
        $mapTypes = [
336 127
            'str' => 'string',
337 127
            'string' => 'string',
338 127
            'bool' => 'boolean',
339 127
            'boolean' => 'boolean',
340 127
            'int' => 'integer',
341 127
            'integer' => 'integer',
342 127
            'float' => 'float',
343 127
            'double' => 'float',
344 127
        ];
345
346
        // compare against different types - first one match, use it
347 127
        $variableType = \gettype($value);
348 127
        $classType = \is_object($value) ? \get_class($value) : null;
349 127
        $parentInstances = $classType ? static::getPropertyParentInstances($classType) : [];
350 127
        foreach ($unionType->getTypes() as $reflectedType) {
351 127
            if (is_a($reflectedType, ReflectionNamedType::class)) {
352 127
                if (in_array($reflectedType->getName(), $parentInstances)) {
353
                    // objects are special in match
354 117
                    return 'object';
355
                }
356
                // in map
357 113
                if (isset($mapTypes[$reflectedType->getName()])) {
358 100
                    return $mapTypes[$reflectedType->getName()];
359
                }
360
                // direct
361 99
                if ($reflectedType->getName() == $variableType) {
362 62
                    return $reflectedType->getName();
363
                }
364
            }
365
        }
366
        // @codeCoverageIgnoreStart
367
        // last fallback when \ReflectionUnionType pass some unrecognizable shit
368
        return null;
369
        // @codeCoverageIgnoreEnd
370
    }
371
372
    /**
373
     * @param class-string $name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
374
     *
375
     * @throws ReflectionException
376
     *
377
     * @return class-string[]
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
378
     */
379 117
    protected static function getPropertyParentInstances(string $name): array
380
    {
381 117
        $reflectionClass = new ReflectionClass($name);
382 117
        $usedClasses = array_merge([$name], $reflectionClass->getInterfaceNames());
383 117
        while ($parentClass = $reflectionClass->getParentClass()) {
384 100
            $usedClasses[] = $parentClass->getName();
385 100
            $usedClasses = $usedClasses + $reflectionClass->getInterfaceNames();
386 100
            $reflectionClass = $parentClass;
387
        }
388 117
        return $usedClasses;
389
    }
390
391
    /**
392
     * Hydrate cloned instance with type control and conversion
393
     *
394
     * @param AbstractDto $clonedInstance
395
     * @param string $key
396
     * @param string $propertyType
397
     * @param mixed $value
398
     * @param ReflectionProperty $property
399
     *
400
     * @return void
401
     */
402 152
    protected static function hydrateClonedInstance(
403
        AbstractDto & $clonedInstance,
404
        string $key,
405
        string $propertyType,
406
        mixed $value,
407
        ReflectionProperty $property,
408
    ): void {
409 152
        $clonedInstance->{$key} = match ($propertyType) {
410 66
            'iterable', 'array' => (array) $value,
411
            'bool' => \is_bool($value) ? $value : (\is_string($value) ? ('true' == $value) : \boolval(\intval($value))),
412 3
            'float', 'double' => \floatval($value),
413 41
            'int' => \intval($value),
414
            'null' => null,
415 121
            'object', 'mixed' => $value,
416 148
            'string' => \strval($value),
417
            'false' => false,
418
            'true' => true,
419
            default => $property->getDefaultValue(),
420 152
        };
421
    }
422
423
    /**
424
     * Hydrate properties which has been set dynamically
425
     *
426
     * @param AbstractDto $clonedInstance
427
     * @param AbstractDto $sourceClass
428
     * @param string[] $usedKeys
429
     * @param array<string, mixed> $data
430
     *
431
     * @return void
432
     */
433 155
    protected static function hydrateDynamicallySetProperties(
434
        AbstractDto & $clonedInstance,
435
        AbstractDto $sourceClass,
436
        array       $usedKeys,
437
        array       $data,
438
    ): void {
439 155
        $properties = \array_diff(\array_keys((array) $sourceClass), $usedKeys);
440 155
        $filledProperties = [];
441 155
        foreach ($properties as $property) {
442
            // cannot determine their metadata, so just copy them
443 4
            if (isset($data[$property])) {
444 4
                $filledProperties[] = $property;
445 4
                $clonedInstance->{$property} = $data[$property];
446
            }
447
        }
448
        // the rest which has not been affected - copy values from the source
449 155
        $unaffectedPropertyKeys = \array_diff($properties, $filledProperties);
450 155
        foreach ($unaffectedPropertyKeys as $unaffectedPropertyKey) {
451 1
            $clonedInstance->{$unaffectedPropertyKey} = $sourceClass->{$unaffectedPropertyKey};
452
        }
453
    }
454
}
455