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 | 7 | public function introspect(string $targetClass): ClassInfo |
|
21 | { |
||
22 | 7 | $instance = new ClassInfo(); |
|
23 | 7 | $instance->setClassName($targetClass); |
|
24 | |||
25 | 7 | $reflection = new ReflectionClass($targetClass); |
|
26 | 7 | $properties = $reflection->getProperties(); |
|
27 | 7 | $instance->setIgnoreUnknown($this->isIgnoreUnknown($reflection)); |
|
28 | |||
29 | 7 | foreach ($properties as $property) { |
|
30 | 7 | $classField = new ClassField(); |
|
31 | 7 | $propertyName = $property->getName(); |
|
32 | |||
33 | 7 | $classField->setName($propertyName); |
|
34 | 7 | $classField->setRequired($this->isRequired($property)); |
|
35 | 7 | $classField->setHasDefaultValue($this->resolveHasDefaultValue($property)); |
|
36 | 7 | $classField->setDefaultValueResolver($this->getDefaultValueResolver($property)); |
|
37 | 7 | $this->resolveType($property, $classField); |
|
38 | 7 | $this->resolveSetter($reflection, $property, $classField); |
|
39 | |||
40 | 7 | $instance->addClassField($propertyName, $classField); |
|
41 | } |
||
42 | |||
43 | 7 | $this->fixCircularReferences($instance); |
|
44 | |||
45 | 7 | return $instance; |
|
46 | } |
||
47 | |||
48 | 7 | private function isIgnoreUnknown(ReflectionClass $reflectionClass): bool |
|
49 | { |
||
50 | 7 | $attributes = $reflectionClass->getAttributes(IgnoreUnknown::class); |
|
51 | |||
52 | 7 | return !empty($attributes); |
|
53 | } |
||
54 | |||
55 | 7 | private function getDefaultValueResolver(ReflectionProperty $reflectionProperty): ?string |
|
56 | { |
||
57 | 7 | $attributes = $reflectionProperty->getAttributes(DefaultValueResolver::class); |
|
58 | 7 | if (!empty($attributes)) { |
|
59 | /** @var DefaultValueResolver $attribute */ |
||
60 | 1 | $attribute = $attributes[0]->newInstance(); |
|
61 | |||
62 | 1 | return $attribute->getResolver(); |
|
63 | } |
||
64 | |||
65 | 6 | return null; |
|
66 | } |
||
67 | |||
68 | 7 | private function getNestedClassInfos(ClassInfo $classInfo): array |
|
69 | { |
||
70 | 7 | $map = []; |
|
71 | |||
72 | 7 | foreach ($classInfo->getFields() as $field) { |
|
73 | 7 | if ($field->getClassInfo() !== null) { |
|
74 | 1 | $map[$field->getType()] = $field->getClassInfo(); |
|
75 | 1 | foreach ($this->getNestedClassInfos($field->getClassInfo()) as $k => $f) { |
|
76 | $map[$k] = $f; |
||
77 | } |
||
78 | } |
||
79 | } |
||
80 | |||
81 | 7 | return $map; |
|
82 | } |
||
83 | |||
84 | 7 | private function resolveSetter(ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, ClassField $classField): void |
|
85 | { |
||
86 | 7 | $isPublic = $reflectionProperty->isPublic(); |
|
87 | 7 | $classField->setIsPublic($isPublic); |
|
88 | |||
89 | 7 | if (true === $isPublic) { |
|
90 | 5 | return; |
|
91 | } |
||
92 | |||
93 | 2 | $setterAttributeOptional = $reflectionProperty->getAttributes(Setter::class); |
|
94 | 2 | if (!empty($setterAttributeOptional)) { |
|
95 | /**@var Setter $setterAttribute */ |
||
96 | 1 | $setterAttribute = $setterAttributeOptional[0]->newInstance(); |
|
97 | 1 | $classField->setSetter($setterAttribute->getSetterName()); |
|
98 | |||
99 | 1 | return; |
|
100 | } |
||
101 | |||
102 | 2 | $possibleSetter = "set" . ucfirst($reflectionProperty->getName()); |
|
103 | 2 | 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 | 2 | $classField->setSetter($possibleSetter); |
|
112 | 2 | } |
|
113 | |||
114 | |||
115 | 7 | private function resolveHasDefaultValue(ReflectionProperty $property): bool |
|
116 | { |
||
117 | 7 | $attributes = $property->getAttributes(HasNotDefaultValue::class); |
|
118 | 7 | if (empty($attributes)) { |
|
119 | 7 | return $property->hasDefaultValue(); |
|
120 | } |
||
121 | |||
122 | 1 | return false; |
|
123 | } |
||
124 | |||
125 | /** |
||
126 | * @throws ReflectionException |
||
127 | */ |
||
128 | 7 | private function resolveType(ReflectionProperty $reflectionProperty, ClassField $classField): void |
|
129 | { |
||
130 | 7 | $type = $reflectionProperty->getType(); |
|
131 | 7 | if (null === $type) { |
|
132 | 1 | $classField->setType('mixed'); |
|
133 | |||
134 | 1 | return; |
|
135 | } |
||
136 | 7 | $typeName = $type->getName(); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
137 | 7 | $isNested = false; |
|
138 | 7 | $isList = false; |
|
139 | 7 | $isCollection = false; |
|
140 | 7 | if (!$type->isBuiltin()) { |
|
141 | $isNested = true; |
||
142 | 7 | } else if ('array' === $type->getName()) { |
|
143 | 5 | $attributes = $reflectionProperty->getAttributes(Collection::class); |
|
144 | 5 | $isList = true; |
|
145 | 5 | if (!empty($attributes)) { |
|
146 | 1 | $isNested = true; |
|
147 | 1 | $isCollection = true; |
|
148 | /** @var Collection $attribute */ |
||
149 | 1 | $attribute = $attributes[0]->newInstance(); |
|
150 | 1 | $typeName = $attribute->getClass(); |
|
151 | } |
||
152 | } |
||
153 | |||
154 | 7 | $classField->setType($typeName); |
|
155 | 7 | $classField->setIsList($isList); |
|
156 | 7 | $classField->setIsTypedCollection($isCollection); |
|
157 | 7 | if ($isNested) { |
|
158 | 1 | if ($typeName === $reflectionProperty->class) { |
|
159 | //prevent loop on nested elements of the same type |
||
160 | return; |
||
161 | } |
||
162 | 1 | $classField->setClassInfo($this->introspect($typeName)); |
|
163 | } |
||
164 | 7 | } |
|
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 | 7 | private function isRequired(ReflectionProperty $reflectionProperty): bool |
|
187 | { |
||
188 | 7 | $attributes = $reflectionProperty->getAttributes(Required::class); |
|
189 | 7 | if (!empty($attributes)) { |
|
190 | 1 | return true; |
|
191 | } |
||
192 | |||
193 | 7 | $propertyType = $reflectionProperty->getType(); |
|
194 | //if property hasn't type hint by default it's equal to null |
||
195 | 7 | if (null !== $propertyType && $reflectionProperty->hasDefaultValue()) { |
|
196 | 1 | return false; |
|
197 | } |
||
198 | |||
199 | 7 | if (null !== $propertyType) { |
|
200 | 7 | return !$propertyType->allowsNull(); |
|
201 | } |
||
202 | |||
203 | 1 | $doc = $reflectionProperty->getDocComment(); |
|
204 | 1 | if (false === $doc) { |
|
205 | return false; |
||
206 | } |
||
207 | |||
208 | 1 | if (preg_match('/@var\s+(\S+)/', $doc, $matches)) { |
|
209 | 1 | [, $type] = $matches; |
|
210 | |||
211 | 1 | return !(str_contains($type, 'null|') or str_contains($type, '|null')); |
|
212 | } |
||
213 | |||
214 | return false; |
||
215 | } |
||
216 | |||
217 | 7 | private function findUnresolved(ClassInfo $classInfo, array $map): void |
|
218 | { |
||
219 | 7 | foreach ($classInfo->getFields() as $field) { |
|
220 | 7 | if ($field->getClassInfo() === null) { |
|
221 | |||
222 | 7 | if (!array_key_exists($field->getType(), $map)) { |
|
223 | 7 | continue; |
|
224 | } |
||
225 | |||
226 | $field->setClassInfo($map[$field->getType()]); |
||
227 | 1 | } else if ($field->getClassInfo() !== null) { |
|
228 | 1 | $this->findUnresolved($field->getClassInfo(), $map); |
|
229 | } |
||
230 | } |
||
231 | 7 | } |
|
232 | |||
233 | 7 | public function fixCircularReferences(ClassInfo $classInfo): void |
|
234 | { |
||
235 | 7 | $map = $this->getNestedClassInfos($classInfo); |
|
236 | 7 | $this->findUnresolved($classInfo, $map); |
|
237 | 7 | } |
|
238 | } |
||
239 |