2be3a24880ca2c7cc9d2d5b16361c2dbde350ca2
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 declare(strict_types=1);
4
5 namespace CuyZ\Valinor\Mapper\Object\Factory;
6
7 use CuyZ\Valinor\Definition\ClassDefinition;
8 use CuyZ\Valinor\Definition\FunctionObject;
9 use CuyZ\Valinor\Definition\FunctionsContainer;
10 use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
11 use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
12 use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorClassTypeParameter;
13 use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorReturnType;
14 use CuyZ\Valinor\Mapper\Object\Exception\MissingConstructorClassTypeParameter;
15 use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
16 use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
17 use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
18 use CuyZ\Valinor\Mapper\Object\NativeEnumObjectBuilder;
19 use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
20 use CuyZ\Valinor\Type\ClassType;
21 use CuyZ\Valinor\Type\ObjectType;
22 use CuyZ\Valinor\Type\Types\ClassStringType;
23 use CuyZ\Valinor\Type\Types\EnumType;
24 use CuyZ\Valinor\Type\Types\NativeStringType;
25
26 use function array_key_exists;
27 use function count;
28 use function is_a;
29
30 /** @internal */
31 final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
32 {
33 /** @var list<FunctionObject> */
34 private array $filteredConstructors;
35
36 public function __construct(
37 private ObjectBuilderFactory $delegate,
38 /** @var array<class-string, null> */
39 private array $nativeConstructors,
40 private FunctionsContainer $constructors
41 ) {
42 }
43
44 public function for(ClassDefinition $class): array
45 {
46 $builders = $this->builders($class);
47
48 if (count($builders) === 0) {
49 if ($class->methods()->hasConstructor()) {
50 throw new CannotInstantiateObject($class);
51 }
52
53 return $this->delegate->for($class);
54 }
55
56 return $builders;
57 }
58
59 /**
60 * @return list<ObjectBuilder>
61 */
62 private function builders(ClassDefinition $class): array
63 {
64 $className = $class->name();
65 $classType = $class->type();
66 $methods = $class->methods();
67
68 $builders = [];
69
70 foreach ($this->filteredConstructors() as $constructor) {
71 if (! $this->constructorMatches($constructor, $classType)) {
72 continue;
73 }
74
75 $definition = $constructor->definition();
76 $functionClass = $definition->class();
77
78 if ($functionClass && $definition->isStatic() && ! $definition->isClosure()) {
79 $scopedClass = is_a($className, $functionClass, true) ? $className : $functionClass;
80
81 $builders[] = new MethodObjectBuilder($scopedClass, $definition->name(), $definition->parameters());
82 } else {
83 $builders[] = new FunctionObjectBuilder($constructor, $classType);
84 }
85 }
86
87 if (! array_key_exists($className, $this->nativeConstructors) && count($builders) > 0) {
88 return $builders;
89 }
90
91 if ($classType instanceof EnumType) {
92 $builders[] = new NativeEnumObjectBuilder($classType);
93 } elseif ($methods->hasConstructor() && $methods->constructor()->isPublic()) {
94 $builders[] = new NativeConstructorObjectBuilder($class);
95 }
96
97 return $builders;
98 }
99
100 private function constructorMatches(FunctionObject $function, ClassType $classType): bool
101 {
102 $definition = $function->definition();
103 $parameters = $definition->parameters();
104 $returnType = $definition->returnType();
105
106 if (! $classType->matches($returnType)) {
107 return false;
108 }
109
110 if (! $definition->attributes()->has(DynamicConstructor::class)) {
111 return true;
112 }
113
114 if (count($parameters) === 0) {
115 throw new MissingConstructorClassTypeParameter($definition);
116 }
117
118 $parameterType = $parameters->at(0)->type();
119
120 if ($parameterType instanceof NativeStringType) {
121 $parameterType = ClassStringType::get();
122 }
123
124 if (! $parameterType instanceof ClassStringType) {
125 throw new InvalidConstructorClassTypeParameter($definition, $parameterType);
126 }
127
128 $subType = $parameterType->subType();
129
130 if ($subType) {
131 return $classType->matches($subType);
132 }
133
134 return true;
135 }
136
137 /**
138 * @return list<FunctionObject>
139 */
140 private function filteredConstructors(): array
141 {
142 if (! isset($this->filteredConstructors)) {
143 $this->filteredConstructors = [];
144
145 foreach ($this->constructors as $constructor) {
146 $function = $constructor->definition();
147
148 if (enum_exists($function->class() ?? '') && in_array($function->name(), ['from', 'tryFrom'], true)) {
149 continue;
150 }
151
152 if (! $function->returnType() instanceof ObjectType) {
153 throw new InvalidConstructorReturnType($function);
154 }
155
156 $this->filteredConstructors[] = $constructor;
157 }
158 }
159
160 return $this->filteredConstructors;
161 }
162 }