Passed
Push — main ( 06b87c...8c7008 )
by Stanislav
02:19
created

ClassInfoReflector   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 101
dl 0
loc 222
ccs 0
cts 113
cp 0
rs 9.52
c 1
b 0
f 0
wmc 36

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isIgnoreUnknown() 0 5 1
A fixCircularReferences() 0 4 1
B resolveType() 0 35 7
A resolveSetter() 0 28 4
A getNestedClassInfos() 0 14 4
A introspect() 0 26 2
A findUnresolved() 0 12 5
A resolveHasDefaultValue() 0 8 2
A getDefaultValueResolver() 0 11 2
B isRequired() 0 29 8
1
<?php declare(strict_types=1);
2
3
namespace Diezz\YamlToObjectMapper;
4
5
use Diezz\YamlToObjectMapper\Attributes\Collection;
6
use Diezz\YamlToObjectMapper\Attributes\DefaultValueResolver;
7
use Diezz\YamlToObjectMapper\Attributes\HasNotDefaultValue;
8
use Diezz\YamlToObjectMapper\Attributes\IgnoreUnknown;
9
use Diezz\YamlToObjectMapper\Attributes\Required;
10
use Diezz\YamlToObjectMapper\Attributes\Setter;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionProperty;
14
15
class ClassInfoReflector
16
{
17
    /**
18
     * @throws ReflectionException
19
     */
20
    public function introspect(string $targetClass): ClassInfo
21
    {
22
        $instance = new ClassInfo();
23
        $instance->setClassName($targetClass);
24
25
        $reflection = new ReflectionClass($targetClass);
26
        $properties = $reflection->getProperties();
27
        $instance->setIgnoreUnknown($this->isIgnoreUnknown($reflection));
28
29
        foreach ($properties as $property) {
30
            $classField = new ClassField();
31
            $propertyName = $property->getName();
32
33
            $classField->setName($propertyName);
34
            $classField->setRequired($this->isRequired($property));
35
            $classField->setHasDefaultValue($this->resolveHasDefaultValue($property));
36
            $classField->setDefaultValueResolver($this->getDefaultValueResolver($property));
37
            $this->resolveType($property, $classField);
38
            $this->resolveSetter($reflection, $property, $classField);
39
40
            $instance->addClassField($propertyName, $classField);
41
        }
42
43
        $this->fixCircularReferences($instance);
44
45
        return $instance;
46
    }
47
48
    private function isIgnoreUnknown(ReflectionClass $reflectionClass): bool
49
    {
50
        $attributes = $reflectionClass->getAttributes(IgnoreUnknown::class);
51
52
        return !empty($attributes);
53
    }
54
55
    private function getDefaultValueResolver(ReflectionProperty $reflectionProperty): ?string
56
    {
57
        $attributes = $reflectionProperty->getAttributes(DefaultValueResolver::class);
58
        if (!empty($attributes)) {
59
            /** @var DefaultValueResolver $attribute */
60
            $attribute = $attributes[0]->newInstance();
61
62
            return $attribute->getResolver();
63
        }
64
65
        return null;
66
    }
67
68
    private function getNestedClassInfos(ClassInfo $classInfo): array
69
    {
70
        $map = [];
71
72
        foreach ($classInfo->getFields() as $field) {
73
            if ($field->getClassInfo() !== null) {
74
                $map[$field->getType()] = $field->getClassInfo();
75
                foreach ($this->getNestedClassInfos($field->getClassInfo()) as $k => $f) {
76
                    $map[$k] = $f;
77
                }
78
            }
79
        }
80
81
        return $map;
82
    }
83
84
    private function resolveSetter(ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, ClassField $classField): void
85
    {
86
        $isPublic = $reflectionProperty->isPublic();
87
        $classField->setIsPublic($isPublic);
88
89
        if (true === $isPublic) {
90
            return;
91
        }
92
93
        $setterAttributeOptional = $reflectionProperty->getAttributes(Setter::class);
94
        if (!empty($setterAttributeOptional)) {
95
            /**@var  Setter $setterAttribute */
96
            $setterAttribute = $setterAttributeOptional[0]->newInstance();
97
            $classField->setSetter($setterAttribute->getSetterName());
98
99
            return;
100
        }
101
102
        $possibleSetter = "set" . ucfirst($reflectionProperty->getName());
103
        if (false === $reflectionClass->hasMethod($possibleSetter)) {
104
            throw new \RuntimeException(sprintf(
105
                "Unable to find suitable setter for property %s for class %s",
106
                $reflectionProperty->getName(),
107
                $reflectionClass->getName()
108
            ));
109
        }
110
111
        $classField->setSetter($possibleSetter);
112
    }
113
114
115
    private function resolveHasDefaultValue(ReflectionProperty $property): bool
116
    {
117
        $attributes = $property->getAttributes(HasNotDefaultValue::class);
118
        if (empty($attributes)) {
119
            return $property->hasDefaultValue();
120
        }
121
122
        return false;
123
    }
124
125
    /**
126
     * @throws ReflectionException
127
     */
128
    private function resolveType(ReflectionProperty $reflectionProperty, ClassField $classField): void
129
    {
130
        $type = $reflectionProperty->getType();
131
        if (null === $type) {
132
            $classField->setType('mixed');
133
134
            return;
135
        }
136
        $typeName = $type->getName();
0 ignored issues
show
Bug introduced by
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

136
        /** @scrutinizer ignore-call */ 
137
        $typeName = $type->getName();
Loading history...
137
        $isNested = false;
138
        $isList = false;
139
        $isCollection = false;
140
        if (!$type->isBuiltin()) {
141
            $isNested = true;
142
        } else if ('array' === $type->getName()) {
143
            $attributes = $reflectionProperty->getAttributes(Collection::class);
144
            $isList = true;
145
            if (!empty($attributes)) {
146
                $isNested = true;
147
                $isCollection = true;
148
                /** @var Collection $attribute */
149
                $attribute = $attributes[0]->newInstance();
150
                $typeName = $attribute->getClass();
151
            }
152
        }
153
154
        $classField->setType($typeName);
155
        $classField->setIsList($isList);
156
        $classField->setIsTypedCollection($isCollection);
157
        if ($isNested) {
158
            if ($typeName === $reflectionProperty->class) {
159
                //prevent loop on nested elements of the same type
160
                return;
161
            }
162
            $classField->setClassInfo($this->introspect($typeName));
163
        }
164
    }
165
166
    /**
167
     * There are should be 3 ways how to figure out whether the field is required or not.
168
     * 1. Use attribute #Required. The most preferable way
169
     * 2. If value has default value it means that fields isn't required
170
     * 3. Use property typehint e.g
171
     * ```
172
     * private int $value
173
     * ```
174
     * would be treated as required and
175
     * ```
176
     * private ?int $value
177
     * ```
178
     * would br treated as optional(non required) field
179
     * 4. phpDoc comment. If doc comment contains `@var` type and the type contains `null|...` or `...|null' that would
180
     * be treated as optional, otherwise as required.
181
     *
182
     * If all three ways doesn't give a result the field would be treated as optional.
183
     *
184
     * @return bool
185
     */
186
    private function isRequired(ReflectionProperty $reflectionProperty): bool
187
    {
188
        $attributes = $reflectionProperty->getAttributes(Required::class);
189
        if (!empty($attributes)) {
190
            return true;
191
        }
192
193
        $propertyType = $reflectionProperty->getType();
194
        //if property hasn't type hint by default it's equal to null
195
        if (null !== $propertyType && $reflectionProperty->hasDefaultValue()) {
196
            return false;
197
        }
198
199
        if (null !== $propertyType) {
200
            return !$propertyType->allowsNull();
201
        }
202
203
        $doc = $reflectionProperty->getDocComment();
204
        if (false === $doc) {
205
            return false;
206
        }
207
208
        if (preg_match('/@var\s+(\S+)/', $doc, $matches)) {
209
            [, $type] = $matches;
210
211
            return !(str_contains($type, 'null|') or str_contains($type, '|null'));
212
        }
213
214
        return false;
215
    }
216
217
    private function findUnresolved(ClassInfo $classInfo, array $map): void
218
    {
219
        foreach ($classInfo->getFields() as $field) {
220
            if ($field->getClassInfo() === null) {
221
222
                if (!array_key_exists($field->getType(), $map)) {
223
                    continue;
224
                }
225
226
                $field->setClassInfo($map[$field->getType()]);
227
            } else if ($field->getClassInfo() !== null) {
228
                $this->findUnresolved($field->getClassInfo(), $map);
229
            }
230
        }
231
    }
232
233
    public function fixCircularReferences(ClassInfo $classInfo): void
234
    {
235
        $map = $this->getNestedClassInfos($classInfo);
236
        $this->findUnresolved($classInfo, $map);
237
    }
238
}
239