1 | <?php |
||
29 | final class GenericEntityCreateController |
||
30 | { |
||
31 | |||
32 | private ControllerHelperInterface $controllerHelper; |
||
|
|||
33 | |||
34 | private ContainerInterface $container; |
||
35 | |||
36 | /** @var class-string */ |
||
37 | private string $entityClass; |
||
38 | |||
39 | /** @var array<string, array<string, mixed>> */ |
||
40 | private array $calls = array(); |
||
41 | |||
42 | private ?string $factory = null; |
||
43 | |||
44 | /** @var array<string, mixed> */ |
||
45 | private array $constructArguments = array(); |
||
46 | |||
47 | private ArgumentCompilerInterface $argumentBuilder; |
||
48 | |||
49 | private string $successResponse; |
||
50 | |||
51 | private ?string $authorizationAttribute; |
||
52 | |||
53 | private ?string $successRedirectRoute; |
||
54 | |||
55 | private array $successRedirectArguments; |
||
56 | |||
57 | private int $successRedirectStatus; |
||
58 | |||
59 | private string $entityIdKey; |
||
60 | |||
61 | private string $entityIdGetter; |
||
62 | |||
63 | public function __construct( |
||
64 | ControllerHelperInterface $controllerHelper, |
||
65 | ArgumentCompilerInterface $argumentBuilder, |
||
66 | ContainerInterface $container, |
||
67 | array $options |
||
68 | ) { |
||
69 | Assert::keyExists($options, 'entity-class'); |
||
70 | |||
71 | /** @var int $defaultRedirectStatus */ |
||
72 | $defaultRedirectStatus = 303; |
||
73 | |||
74 | $options = array_merge([ |
||
75 | 'calls' => [], |
||
76 | 'success-response' => "object created", |
||
77 | 'factory' => null, |
||
78 | 'authorization-attribute' => null, |
||
79 | 'arguments' => [], |
||
80 | 'success-redirect' => null, |
||
81 | 'success-redirect-arguments' => [], |
||
82 | 'success-redirect-status' => $defaultRedirectStatus, |
||
83 | 'entity-id-getter' => method_exists($options['entity-class'], 'id') ?'id' :'getId', |
||
84 | 'entity-id-key' => 'entityId', |
||
85 | ], $options); |
||
86 | |||
87 | $this->controllerHelper = $controllerHelper; |
||
88 | $this->argumentBuilder = $argumentBuilder; |
||
89 | $this->container = $container; |
||
90 | $this->entityClass = $options['entity-class']; |
||
91 | $this->entityIdGetter = $options['entity-id-getter']; |
||
92 | $this->entityIdKey = $options['entity-id-key']; |
||
93 | $this->successResponse = $options['success-response']; |
||
94 | $this->factory = $options['factory']; |
||
95 | $this->authorizationAttribute = $options['authorization-attribute']; |
||
96 | $this->constructArguments = $options['arguments']; |
||
97 | $this->successRedirectRoute = $options['success-redirect']; |
||
98 | $this->successRedirectArguments = $options['success-redirect-arguments']; |
||
99 | $this->successRedirectStatus = $options['success-redirect-status']; |
||
100 | |||
101 | foreach ($options['calls'] as $methodName => $arguments) { |
||
102 | 22 | /** @var array $arguments */ |
|
103 | |||
104 | Assert::isArray($arguments); |
||
105 | Assert::true(method_exists($this->entityClass, $methodName)); |
||
106 | |||
107 | $this->calls[$methodName] = $arguments; |
||
108 | 22 | } |
|
109 | 22 | } |
|
110 | |||
111 | public function __invoke(): Response |
||
112 | 21 | { |
|
113 | /** @var Request $request */ |
||
114 | 21 | $request = $this->controllerHelper->getCurrentRequest(); |
|
115 | 21 | ||
116 | 21 | Assert::isInstanceOf($request, Request::class, "Cannot use controller outside of request-scope!"); |
|
117 | |||
118 | return $this->createEntity($request); |
||
119 | } |
||
120 | |||
121 | public function createEntity(Request $request): Response |
||
122 | 21 | { |
|
123 | 21 | /** @var object $factoryObject */ |
|
124 | 21 | $factoryObject = null; |
|
125 | 21 | ||
126 | if (!empty($this->authorizationAttribute)) { |
||
127 | 21 | $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $request); |
|
128 | 21 | } |
|
129 | 21 | ||
130 | 21 | /** @var ReflectionFunctionAbstract|null $constructorReflection */ |
|
131 | 21 | $constructorReflection = $this->findConstructorReflection($factoryObject); |
|
132 | 21 | ||
133 | 21 | /** @var array<int, mixed> $constructArguments */ |
|
134 | 21 | $constructArguments = array(); |
|
135 | 21 | ||
136 | 21 | if ($constructorReflection instanceof ReflectionFunctionAbstract) { |
|
137 | 21 | $constructArguments = $this->argumentBuilder->buildCallArguments( |
|
138 | 21 | $constructorReflection, |
|
139 | 21 | $this->constructArguments |
|
140 | ); |
||
141 | 21 | ||
142 | /** @var object $entity */ |
||
143 | $entity = $this->createEntityByConstructor($constructorReflection, $constructArguments, $factoryObject); |
||
144 | 4 | ||
145 | 3 | } else { |
|
146 | /** @var class-string $entityClass */ |
||
147 | 1 | $entityClass = $this->entityClass; |
|
148 | |||
149 | 18 | $entity = new $entityClass(); |
|
150 | } |
||
151 | 2 | ||
152 | $this->performPostCreationCalls($entity); |
||
153 | |||
154 | 2 | if (!empty($this->authorizationAttribute)) { |
|
155 | $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $entity); |
||
156 | 2 | } |
|
157 | |||
158 | 1 | $this->controllerHelper->persistEntity($entity); |
|
159 | |||
160 | $this->controllerHelper->dispatchEvent("symfony_generics.entity_interaction", new EntityInteractionEvent( |
||
161 | 16 | $this->entityClass, |
|
162 | null, # TODO: get id via reflection |
||
163 | $entity, |
||
164 | 16 | "__construct", |
|
165 | $constructArguments |
||
166 | 16 | )); |
|
167 | 1 | ||
168 | $this->controllerHelper->flushORM(); |
||
169 | |||
170 | if (!empty($this->successRedirectRoute)) { |
||
171 | 16 | /** @var array $redirectArguments */ |
|
172 | $redirectArguments = $this->argumentBuilder->buildArguments( |
||
173 | $this->successRedirectArguments |
||
174 | 12 | ); |
|
175 | |||
176 | 12 | /** @var callable $idGetterCallback */ |
|
177 | 11 | $idGetterCallback = [$entity, $this->entityIdGetter]; |
|
178 | 11 | ||
179 | 11 | $redirectArguments[$this->entityIdKey] = call_user_func($idGetterCallback); |
|
180 | |||
181 | return $this->controllerHelper->redirectToRoute( |
||
182 | $this->successRedirectRoute, |
||
183 | 11 | $redirectArguments, |
|
184 | $this->successRedirectStatus |
||
185 | ); |
||
186 | } |
||
187 | 1 | ||
188 | return new Response($this->successResponse, 200); |
||
189 | 1 | } |
|
190 | |||
191 | private function findConstructorReflection(object &$factoryObject = null): ?ReflectionFunctionAbstract |
||
192 | 11 | { |
|
193 | /** @var ReflectionFunctionAbstract|null $constructorReflection */ |
||
194 | 11 | $constructorReflection = null; |
|
195 | 1 | ||
196 | if (!empty($this->factory)) { |
||
197 | if (is_int(strpos($this->factory, '::'))) { |
||
198 | 10 | [$factoryClass, $factoryMethod] = explode('::', $this->factory, 2); |
|
199 | |||
200 | 10 | if (!empty($factoryClass)) { |
|
201 | 10 | if ($factoryClass[0] == '@') { |
|
202 | 10 | # Create by factory-service-object |
|
203 | 10 | ||
204 | 10 | $factoryObject = $this->container->get(substr($factoryClass, 1)); |
|
205 | 10 | ||
206 | Assert::object($factoryObject, sprintf( |
||
207 | "Did not find service '%s'!", |
||
208 | 10 | substr($factoryClass, 1) |
|
209 | )); |
||
210 | 10 | ||
211 | $constructorReflection = (new ReflectionObject($factoryObject))->getMethod($factoryMethod); |
||
212 | 1 | ||
213 | 1 | } else { |
|
214 | # Create by static factory-method of other class |
||
215 | |||
216 | Assert::classExists($factoryClass); |
||
217 | 1 | ||
218 | $constructorReflection = (new ReflectionClass($factoryClass))->getMethod($factoryMethod); |
||
219 | 1 | } |
|
220 | |||
221 | 1 | } else { |
|
222 | 1 | throw new ErrorException(sprintf( |
|
223 | 1 | "Invalid constructor definition: '%s'!", |
|
224 | 1 | $this->factory |
|
225 | )); |
||
226 | } |
||
227 | |||
228 | 9 | } elseif (method_exists($this->entityClass, $this->factory)) { |
|
229 | # Create by static factory method on entity class |
||
230 | |||
231 | $constructorReflection = (new ReflectionClass($this->entityClass))->getMethod($this->factory); |
||
232 | |||
233 | } elseif (function_exists($this->factory)) { |
||
234 | 16 | # Create by factory function |
|
235 | |||
236 | $constructorReflection = new ReflectionFunction($this->factory); |
||
237 | 16 | } |
|
238 | |||
239 | 16 | } else { |
|
240 | 10 | # Create by calling the constructor directly |
|
241 | 7 | ||
242 | $constructorReflection = (new ReflectionClass($this->entityClass))->getConstructor(); |
||
243 | 7 | } |
|
244 | 6 | ||
245 | return $constructorReflection; |
||
246 | } |
||
247 | 6 | ||
248 | private function createEntityByConstructor( |
||
249 | 6 | ReflectionFunctionAbstract $constructorReflection, |
|
250 | 6 | array $constructArguments, |
|
251 | 6 | ?object $factoryObject |
|
252 | ): ?object { |
||
253 | /** @var object|null $entity */ |
||
254 | 5 | $entity = null; |
|
255 | |||
256 | if ($constructorReflection instanceof ReflectionMethod) { |
||
257 | if ($constructorReflection->isConstructor()) { |
||
258 | $entity = $constructorReflection->getDeclaringClass()->newInstanceArgs($constructArguments); |
||
259 | 3 | ||
260 | } elseif ($constructorReflection->isStatic()) { |
||
261 | $entity = $constructorReflection->invokeArgs(null, $constructArguments); |
||
262 | |||
263 | 1 | } else { |
|
264 | 1 | $entity = $constructorReflection->invokeArgs($factoryObject, $constructArguments); |
|
265 | 4 | } |
|
266 | |||
267 | } elseif ($constructorReflection instanceof ReflectionFunction) { |
||
268 | $entity = $constructorReflection->invokeArgs($constructArguments); |
||
269 | 3 | } |
|
270 | |||
271 | Assert::isInstanceOf($entity, $this->entityClass); |
||
272 | 2 | ||
273 | return $entity; |
||
274 | 1 | } |
|
275 | |||
276 | /** |
||
277 | 6 | * @param object $entity |
|
278 | */ |
||
279 | private function performPostCreationCalls($entity): void |
||
280 | { |
||
281 | $classReflection = new ReflectionClass($this->entityClass); |
||
282 | |||
283 | 6 | foreach ($this->calls as $methodName => $callArgumentConfiguration) { |
|
284 | /** @var array $callArgumentConfiguration */ |
||
285 | |||
286 | 12 | /** @var ReflectionMethod $methodReflection */ |
|
287 | $methodReflection = $classReflection->getMethod($methodName); |
||
288 | |||
289 | $callArguments = $this->argumentBuilder->buildCallArguments( |
||
290 | $methodReflection, |
||
291 | $callArgumentConfiguration |
||
292 | ); |
||
293 | |||
294 | 11 | $methodReflection->invokeArgs($entity, $callArguments); |
|
295 | } |
||
296 | } |
||
297 | |||
298 | } |
||
299 |