Completed
Push — master ( 2da628...3624d1 )
by Gerrit
07:01
created

performPostCreationCalls()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.6333
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * Copyright (C) 2018 Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 *
8
 * @license GPL-3.0
9
 *
10
 * @author Gerrit Addiks <[email protected]>
11
 */
12
13
namespace Addiks\SymfonyGenerics\Controllers\API;
14
15
use Addiks\SymfonyGenerics\Controllers\ControllerHelperInterface;
16
use Addiks\SymfonyGenerics\Services\ArgumentCompilerInterface;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\HttpFoundation\Request;
19
use Webmozart\Assert\Assert;
20
use ReflectionClass;
21
use ReflectionMethod;
22
use Psr\Container\ContainerInterface;
23
use ErrorException;
24
use ReflectionFunction;
25
use ReflectionFunctionAbstract;
26
use ReflectionObject;
27
use Addiks\SymfonyGenerics\Events\EntityInteractionEvent;
28
29
final class GenericEntityCreateController
30
{
31
32
    /**
33
     * @var ControllerHelperInterface
34
     */
35
    private $controllerHelper;
36
37
    /**
38
     * @var ContainerInterface
39
     */
40
    private $container;
41
42
    /**
43
     * @var string
44
     */
45
    private $entityClass;
46
47
    /**
48
     * @var array<string, array<string, mixed>>
49
     */
50
    private $calls = array();
51
52
    /**
53
     * @var string|null
54
     */
55
    private $factory = null;
56
57
    /**
58
     * @var array<string, mixed>
59
     */
60
    private $constructArguments = array();
61
62
    /**
63
     * @var ArgumentCompilerInterface
64
     */
65
    private $argumentBuilder;
66
67
    /**
68
     * @var string
69
     */
70
    private $successResponse;
71
72
    /**
73
     * @var string|null
74
     */
75
    private $authorizationAttribute;
76
77
    /**
78
     * @var string|null
79
     */
80
    private $successRedirectRoute;
81
82
    /**
83
     * @var array
84
     */
85
    private $successRedirectArguments;
86
87
    /**
88
     * @var int
89
     */
90
    private $successRedirectStatus;
91
92
    /**
93
     * @var string
94
     */
95
    private $entityIdKey;
96
97
    /**
98
     * @var string
99
     */
100
    private $entityIdGetter;
101
102 19
    public function __construct(
103
        ControllerHelperInterface $controllerHelper,
104
        ArgumentCompilerInterface $argumentBuilder,
105
        ContainerInterface $container,
106
        array $options
107
    ) {
108 19
        Assert::null($this->controllerHelper);
109 19
        Assert::keyExists($options, 'entity-class');
110
111
        /** @var int $defaultRedirectStatus */
112 18
        $defaultRedirectStatus = 303;
113
114 18
        $options = array_merge([
115 18
            'calls' => [],
116 18
            'success-response' => "object created",
117
            'factory' => null,
118
            'authorization-attribute' => null,
119
            'arguments' => [],
120
            'success-redirect' => null,
121
            'success-redirect-arguments' => [],
122 18
            'success-redirect-status' => $defaultRedirectStatus,
123 18
            'entity-id-getter' => 'getId',
124 18
            'entity-id-key' => 'entityId',
125 18
        ], $options);
126
127 18
        $this->controllerHelper = $controllerHelper;
128 18
        $this->argumentBuilder = $argumentBuilder;
129 18
        $this->container = $container;
130 18
        $this->entityClass = $options['entity-class'];
131 18
        $this->entityIdGetter = $options['entity-id-getter'];
132 18
        $this->entityIdKey = $options['entity-id-key'];
133 18
        $this->successResponse = $options['success-response'];
134 18
        $this->factory = $options['factory'];
135 18
        $this->authorizationAttribute = $options['authorization-attribute'];
136 18
        $this->constructArguments = $options['arguments'];
137 18
        $this->successRedirectRoute = $options['success-redirect'];
138 18
        $this->successRedirectArguments = $options['success-redirect-arguments'];
139 18
        $this->successRedirectStatus = $options['success-redirect-status'];
140
141 18
        foreach ($options['calls'] as $methodName => $arguments) {
142
            /** @var array $arguments */
143
144 4
            Assert::isArray($arguments);
145 3
            Assert::true(method_exists($this->entityClass, $methodName));
146
147 1
            $this->calls[$methodName] = $arguments;
148
        }
149 15
    }
150
151 2
    public function __invoke(): Response
152
    {
153
        /** @var Request $request */
154 2
        $request = $this->controllerHelper->getCurrentRequest();
155
156 2
        Assert::isInstanceOf($request, Request::class, "Cannot use controller outside of request-scope!");
157
158 1
        return $this->createEntity($request);
159
    }
160
161 13
    public function createEntity(Request $request): Response
162
    {
163
        /** @var object|null $factoryObject */
164 13
        $factoryObject = null;
165
166 13
        if (!empty($this->authorizationAttribute)) {
167 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $request);
168
        }
169
170
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
171 13
        $constructorReflection = $this->findConstructorReflection($factoryObject);
172
173
        /** @var array<int, mixed> $constructArguments */
174 9
        $constructArguments = array();
175
176 9
        if ($constructorReflection instanceof ReflectionFunctionAbstract) {
177 8
            $constructArguments = $this->argumentBuilder->buildCallArguments(
178 8
                $constructorReflection,
179 8
                $this->constructArguments,
180 8
                $request
181
            );
182
183
            /** @var object $entity */
184 8
            $entity = $this->createEntityByConstructor($constructorReflection, $constructArguments, $factoryObject);
185
186
        } else {
187
            /** @var string $entityClass */
188 1
            $entityClass = $this->entityClass;
189
190 1
            $entity = new $entityClass();
191
        }
192
193 8
        $this->performPostCreationCalls($entity, $request);
194
195 8
        if (!empty($this->authorizationAttribute)) {
196 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $entity);
197
        }
198
199 7
        $this->controllerHelper->persistEntity($entity);
200
201 7
        $this->controllerHelper->dispatchEvent("symfony_generics.entity_interaction", new EntityInteractionEvent(
202 7
            $this->entityClass,
203 7
            null, # TODO: get id via reflection
204 7
            $entity,
205 7
            "__construct",
206 7
            $constructArguments
207
        ));
208
209 7
        $this->controllerHelper->flushORM();
210
211 7
        if (!empty($this->successRedirectRoute)) {
212
            /** @var array $redirectArguments */
213 1
            $redirectArguments = $this->argumentBuilder->buildArguments(
214 1
                $this->successRedirectArguments,
215 1
                $request
216
            );
217
218
            /** @var callable $idGetterCallback */
219 1
            $idGetterCallback = [$entity, $this->entityIdGetter];
220
221 1
            $redirectArguments[$this->entityIdKey] = call_user_func($idGetterCallback);
222
223 1
            return $this->controllerHelper->redirectToRoute(
224 1
                $this->successRedirectRoute,
225 1
                $redirectArguments,
226 1
                $this->successRedirectStatus
227
            );
228
        }
229
230 6
        return new Response($this->successResponse, 200);
231
    }
232
233
    /**
234
     * @param object $factoryObject
235
     */
236 13
    private function findConstructorReflection(&$factoryObject = null): ?ReflectionFunctionAbstract
237
    {
238
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
239 13
        $constructorReflection = null;
240
241 13
        if (!empty($this->factory)) {
242 7
            if (is_int(strpos($this->factory, '::'))) {
243 7
                [$factoryClass, $factoryMethod] = explode('::', $this->factory, 2);
0 ignored issues
show
Bug introduced by
The variable $factoryClass does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $factoryMethod does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
244
245 7
                if (!empty($factoryClass)) {
246 6
                    if ($factoryClass[0] == '@') {
247
                        # Create by factory-service-object
248
249 6
                        $factoryObject = $this->container->get(substr($factoryClass, 1));
250
251 6
                        Assert::object($factoryObject, sprintf(
252 6
                            "Did not find service '%s'!",
253 6
                            substr($factoryClass, 1)
254
                        ));
255
256 5
                        $constructorReflection = (new ReflectionObject($factoryObject))->getMethod($factoryMethod);
257
258
                    } else {
259
                        # Create by static factory-method of other class
260
261 3
                        $constructorReflection = (new ReflectionClass($factoryClass))->getMethod($factoryMethod);
262
                    }
263
264
                } else {
265 1
                    throw new ErrorException(sprintf(
266 1
                        "Invalid constructor definition: '%s'!",
267 4
                        $this->factory
268
                    ));
269
                }
270
271
            } elseif (method_exists($this->entityClass, $this->factory)) {
272
                # Create by static factory method on entity class
273
274
                $constructorReflection = (new ReflectionClass($this->entityClass))->getMethod($this->factory);
275
276
            } elseif (function_exists($this->factory)) {
277
                # Create by factory function
278
279 3
                $constructorReflection = new ReflectionFunction($this->factory);
280
            }
281
282
        } else {
283
            # Create by calling the constructor directly
284
285 6
            $constructorReflection = (new ReflectionClass($this->entityClass))->getConstructor();
286
        }
287
288 9
        return $constructorReflection;
289
    }
290
291
    /**
292
     * @param object|null $factoryObject
293
     *
294
     * @return object
295
     */
296 8
    private function createEntityByConstructor(
297
        ReflectionFunctionAbstract $constructorReflection,
298
        array $constructArguments,
299
        $factoryObject
300
    ) {
301
        /** @var object|null $entity */
302 8
        $entity = null;
303
304 8
        if ($constructorReflection instanceof ReflectionMethod) {
305 8
            if ($constructorReflection->isConstructor()) {
306 5
                $entity = $constructorReflection->getDeclaringClass()->newInstanceArgs($constructArguments);
307
308 3
            } elseif ($constructorReflection->isStatic()) {
309
                $entity = $constructorReflection->invokeArgs(null, $constructArguments);
310
311
            } else {
312 8
                $entity = $constructorReflection->invokeArgs($factoryObject, $constructArguments);
313
            }
314
315
        } elseif ($constructorReflection instanceof ReflectionFunction) {
316
            $entity = $constructorReflection->invokeArgs($constructArguments);
317
        }
318
319 8
        Assert::isInstanceOf($entity, $this->entityClass);
320
321 7
        return $entity;
322
    }
323
324
    /**
325
     * @param object $entity
326
     */
327 8
    private function performPostCreationCalls($entity, Request $request): void
328
    {
329 8
        $classReflection = new ReflectionClass($this->entityClass);
330
331 8
        foreach ($this->calls as $methodName => $callArgumentConfiguration) {
332
            /** @var array $callArgumentConfiguration */
333
334
            /** @var ReflectionMethod $methodReflection */
335 1
            $methodReflection = $classReflection->getMethod($methodName);
336
337 1
            $callArguments = $this->argumentBuilder->buildCallArguments(
338 1
                $methodReflection,
339 1
                $callArgumentConfiguration,
340 1
                $request
341
            );
342
343 1
            $methodReflection->invokeArgs($entity, $callArguments);
344
        }
345 8
    }
346
347
}
348