Processing::getProperties()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

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