7824749bd1b7ebc56eeb78305f0aaed9a0500b09
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 declare(strict_types=1);
4
5 namespace CuyZ\Valinor\Definition\Repository\Reflection;
6
7 use CuyZ\Valinor\Definition\ClassDefinition;
8 use CuyZ\Valinor\Definition\Exception\ClassTypeAliasesDuplication;
9 use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClass;
10 use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClassType;
11 use CuyZ\Valinor\Definition\Exception\UnknownTypeAliasImport;
12 use CuyZ\Valinor\Definition\MethodDefinition;
13 use CuyZ\Valinor\Definition\Methods;
14 use CuyZ\Valinor\Definition\Properties;
15 use CuyZ\Valinor\Definition\PropertyDefinition;
16 use CuyZ\Valinor\Definition\Repository\AttributesRepository;
17 use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
18 use CuyZ\Valinor\Type\ClassType;
19 use CuyZ\Valinor\Type\GenericType;
20 use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
21 use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
22 use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
23 use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification;
24 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
25 use CuyZ\Valinor\Type\Parser\TypeParser;
26 use CuyZ\Valinor\Type\Type;
27 use CuyZ\Valinor\Type\Types\UnresolvableType;
28 use CuyZ\Valinor\Utility\Reflection\Reflection;
29 use ReflectionClass;
30 use ReflectionMethod;
31 use ReflectionProperty;
32 use CuyZ\Valinor\Utility\Reflection\DocParser;
33
34 use function array_filter;
35 use function array_keys;
36 use function array_map;
37
38 /** @internal */
39 final class ReflectionClassDefinitionRepository implements ClassDefinitionRepository
40 {
41 private TypeParserFactory $typeParserFactory;
42
43 private AttributesRepository $attributesFactory;
44
45 private ReflectionPropertyDefinitionBuilder $propertyBuilder;
46
47 private ReflectionMethodDefinitionBuilder $methodBuilder;
48
49 /** @var array<string, ReflectionTypeResolver> */
50 private array $typeResolver = [];
51
52 public function __construct(TypeParserFactory $typeParserFactory, AttributesRepository $attributesFactory)
53 {
54 $this->typeParserFactory = $typeParserFactory;
55 $this->attributesFactory = $attributesFactory;
56 $this->propertyBuilder = new ReflectionPropertyDefinitionBuilder($attributesFactory);
57 $this->methodBuilder = new ReflectionMethodDefinitionBuilder($attributesFactory);
58 }
59
60 public function for(ClassType $type): ClassDefinition
61 {
62 $reflection = Reflection::class($type->className());
63
64 return new ClassDefinition(
65 $type,
66 $this->attributesFactory->for($reflection),
67 new Properties(...$this->properties($type)),
68 new Methods(...$this->methods($type)),
69 $reflection->isFinal(),
70 $reflection->isAbstract(),
71 );
72 }
73
74 /**
75 * @return list<PropertyDefinition>
76 */
77 private function properties(ClassType $type): array
78 {
79 return array_map(
80 function (ReflectionProperty $property) use ($type) {
81 $typeResolver = $this->typeResolver($type, $property->getDeclaringClass());
82
83 return $this->propertyBuilder->for($property, $typeResolver);
84 },
85 Reflection::class($type->className())->getProperties()
86 );
87 }
88
89 /**
90 * @return list<MethodDefinition>
91 */
92 private function methods(ClassType $type): array
93 {
94 $reflection = Reflection::class($type->className());
95 $methods = $reflection->getMethods();
96
97 // Because `ReflectionMethod::getMethods()` wont list the constructor if
98 // it comes from a parent class AND is not public, we need to manually
99 // fetch it and add it to the list.
100 if ($reflection->hasMethod('__construct')) {
101 $methods[] = $reflection->getMethod('__construct');
102 }
103
104 return array_map(function (ReflectionMethod $method) use ($type) {
105 $typeResolver = $this->typeResolver($type, $method->getDeclaringClass());
106
107 return $this->methodBuilder->for($method, $typeResolver);
108 }, $methods);
109 }
110
111 /**
112 * @param ReflectionClass<object> $target
113 */
114 private function typeResolver(ClassType $type, ReflectionClass $target): ReflectionTypeResolver
115 {
116 $typeKey = $target->isInterface()
117 ? "{$type->toString()}/{$type->className()}"
118 : "{$type->toString()}/$target->name";
119
120 if (isset($this->typeResolver[$typeKey])) {
121 return $this->typeResolver[$typeKey];
122 }
123
124 while ($type->className() !== $target->name) {
125 $type = $type->parent();
126 }
127
128 $generics = $type instanceof GenericType ? $type->generics() : [];
129 $localAliases = $this->localTypeAliases($type);
130 $importedAliases = $this->importedTypeAliases($type);
131
132 $duplicates = [];
133 $keys = [...array_keys($generics), ...array_keys($localAliases), ...array_keys($importedAliases)];
134
135 foreach ($keys as $key) {
136 $sameKeys = array_filter($keys, fn ($value) => $value === $key);
137
138 if (count($sameKeys) > 1) {
139 $duplicates[$key] = null;
140 }
141 }
142
143 if (count($duplicates) > 0) {
144 throw new ClassTypeAliasesDuplication($type->className(), ...array_keys($duplicates));
145 }
146
147 $advancedParser = $this->typeParserFactory->get(
148 new ClassContextSpecification($type->className()),
149 new AliasSpecification(Reflection::class($type->className())),
150 new TypeAliasAssignerSpecification($generics + $localAliases + $importedAliases)
151 );
152
153 $nativeParser = $this->typeParserFactory->get(
154 new ClassContextSpecification($type->className())
155 );
156
157 return $this->typeResolver[$typeKey] = new ReflectionTypeResolver($nativeParser, $advancedParser);
158 }
159
160 /**
161 * @return array<string, Type>
162 */
163 private function localTypeAliases(ClassType $type): array
164 {
165 $reflection = Reflection::class($type->className());
166 $rawTypes = DocParser::localTypeAliases($reflection);
167
168 $typeParser = $this->typeParser($type);
169
170 $types = [];
171
172 foreach ($rawTypes as $name => $raw) {
173 try {
174 $types[$name] = $typeParser->parse($raw);
175 } catch (InvalidType $exception) {
176 $raw = trim($raw);
177
178 $types[$name] = UnresolvableType::forLocalAlias($raw, $name, $type, $exception);
179 }
180 }
181
182 return $types;
183 }
184
185 /**
186 * @return array<string, Type>
187 */
188 private function importedTypeAliases(ClassType $type): array
189 {
190 $reflection = Reflection::class($type->className());
191 $importedTypesRaw = DocParser::importedTypeAliases($reflection);
192
193 $typeParser = $this->typeParser($type);
194
195 $importedTypes = [];
196
197 foreach ($importedTypesRaw as $class => $types) {
198 try {
199 $classType = $typeParser->parse($class);
200 } catch (InvalidType) {
201 throw new InvalidTypeAliasImportClass($type, $class);
202 }
203
204 if (! $classType instanceof ClassType) {
205 throw new InvalidTypeAliasImportClassType($type, $classType);
206 }
207
208 $localTypes = $this->localTypeAliases($classType);
209
210 foreach ($types as $importedType) {
211 if (! isset($localTypes[$importedType])) {
212 throw new UnknownTypeAliasImport($type, $classType->className(), $importedType);
213 }
214
215 $importedTypes[$importedType] = $localTypes[$importedType];
216 }
217 }
218
219 return $importedTypes;
220 }
221
222 private function typeParser(ClassType $type): TypeParser
223 {
224 $specs = [
225 new ClassContextSpecification($type->className()),
226 new AliasSpecification(Reflection::class($type->className())),
227 ];
228
229 if ($type instanceof GenericType) {
230 $specs[] = new TypeAliasAssignerSpecification($type->generics());
231 }
232
233 return $this->typeParserFactory->get(...$specs);
234 }
235 }