feff612f5c90bc972daa1300689524e7a32d4fc0
[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 public function for(ClassDefinition $class): array
44 {
45 $builders = $this->builders($class);
46
47 if (count($builders) === 0) {
48 if ($class->methods()->hasConstructor()) {
49 throw new CannotInstantiateObject($class);
50 }
51
52 return $this->delegate->for($class);
53 }
54
55 return $builders;
56 }
57
58 /**
59 * @return list<ObjectBuilder>
60 */
61 private function builders(ClassDefinition $class): array
62 {
63 $className = $class->name();
64 $classType = $class->type();
65 $methods = $class->methods();
66
67 $builders = [];
68
69 foreach ($this->filteredConstructors() as $constructor) {
70 if (! $this->constructorMatches($constructor, $classType)) {
71 continue;
72 }
73
74 $definition = $constructor->definition();
75 $functionClass = $definition->class();
76
77 if ($functionClass && $definition->isStatic() && ! $definition->isClosure()) {
78 $scopedClass = is_a($className, $functionClass, true) ? $className : $functionClass;
79
80 $builders[] = new MethodObjectBuilder($scopedClass, $definition->name(), $definition->parameters());
81 } else {
82 $builders[] = new FunctionObjectBuilder($constructor, $classType);
83 }
84 }
85
86 if (! array_key_exists($className, $this->nativeConstructors) && count($builders) > 0) {
87 return $builders;
88 }
89
90 if ($classType instanceof EnumType) {
91 $builders[] = new NativeEnumObjectBuilder($classType);
92 } elseif ($methods->hasConstructor() && $methods->constructor()->isPublic()) {
93 $builders[] = new NativeConstructorObjectBuilder($class);
94 }
95
96 return $builders;
97 }
98
99 private function constructorMatches(FunctionObject $function, ClassType $classType): bool
100 {
101 $definition = $function->definition();
102 $parameters = $definition->parameters();
103 $returnType = $definition->returnType();
104
105 if (! $classType->matches($returnType)) {
106 return false;
107 }
108
109 if (! $definition->attributes()->has(DynamicConstructor::class)) {
110 return true;
111 }
112
113 if (count($parameters) === 0) {
114 throw new MissingConstructorClassTypeParameter($definition);
115 }
116
117 $parameterType = $parameters->at(0)->type();
118
119 if ($parameterType instanceof NativeStringType) {
120 $parameterType = ClassStringType::get();
121 }
122
123 if (! $parameterType instanceof ClassStringType) {
124 throw new InvalidConstructorClassTypeParameter($definition, $parameterType);
125 }
126
127 $subType = $parameterType->subType();
128
129 if ($subType) {
130 return $classType->matches($subType);
131 }
132
133 return true;
134 }
135
136 /**
137 * @return list<FunctionObject>
138 */
139 private function filteredConstructors(): array
140 {
141 if (! isset($this->filteredConstructors)) {
142 $this->filteredConstructors = [];
143
144 foreach ($this->constructors as $constructor) {
145 $function = $constructor->definition();
146
147 if (enum_exists($function->class() ?? '') && in_array($function->name(), ['from', 'tryFrom'], true)) {
148 continue;
149 }
150
151 if (! $function->returnType() instanceof ObjectType) {
152 throw new InvalidConstructorReturnType($function);
153 }
154
155 $this->filteredConstructors[] = $constructor;
156 }
157 }
158
159 return $this->filteredConstructors;
160 }
161 }