Passed
Push — master ( 50ffe0...5d273a )
by Gerrit
02:07
created

GenericEntityCreateController::__invoke()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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 22
    public function __construct(
103
        ControllerHelperInterface $controllerHelper,
104
        ArgumentCompilerInterface $argumentBuilder,
105
        ContainerInterface $container,
106
        array $options
107
    ) {
108 22
        Assert::null($this->controllerHelper);
109 22
        Assert::keyExists($options, 'entity-class');
110
111
        /** @var int $defaultRedirectStatus */
112 21
        $defaultRedirectStatus = 303;
113
114 21
        $options = array_merge([
115 21
            'calls' => [],
116 21
            'success-response' => "object created",
117
            'factory' => null,
118
            'authorization-attribute' => null,
119
            'arguments' => [],
120
            'success-redirect' => null,
121
            'success-redirect-arguments' => [],
122 21
            'success-redirect-status' => $defaultRedirectStatus,
123 21
            'entity-id-getter' => 'getId',
124 21
            'entity-id-key' => 'entityId',
125 21
        ], $options);
126
127 21
        $this->controllerHelper = $controllerHelper;
128 21
        $this->argumentBuilder = $argumentBuilder;
129 21
        $this->container = $container;
130 21
        $this->entityClass = $options['entity-class'];
131 21
        $this->entityIdGetter = $options['entity-id-getter'];
132 21
        $this->entityIdKey = $options['entity-id-key'];
133 21
        $this->successResponse = $options['success-response'];
134 21
        $this->factory = $options['factory'];
135 21
        $this->authorizationAttribute = $options['authorization-attribute'];
136 21
        $this->constructArguments = $options['arguments'];
137 21
        $this->successRedirectRoute = $options['success-redirect'];
138 21
        $this->successRedirectArguments = $options['success-redirect-arguments'];
139 21
        $this->successRedirectStatus = $options['success-redirect-status'];
140
141 21
        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 18
    }
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 16
    public function createEntity(Request $request): Response
162
    {
163
        /** @var object|null $factoryObject */
164 16
        $factoryObject = null;
165
166 16
        if (!empty($this->authorizationAttribute)) {
167 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $request);
168
        }
169
170
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
171 16
        $constructorReflection = $this->findConstructorReflection($factoryObject);
172
173
        /** @var array<int, mixed> $constructArguments */
174 12
        $constructArguments = array();
175
176 12
        if ($constructorReflection instanceof ReflectionFunctionAbstract) {
177 11
            $constructArguments = $this->argumentBuilder->buildCallArguments(
178 11
                $constructorReflection,
179 11
                $this->constructArguments,
180 11
                $request
181
            );
182
183
            /** @var object $entity */
184 11
            $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 11
        $this->performPostCreationCalls($entity, $request);
194
195 11
        if (!empty($this->authorizationAttribute)) {
196 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $entity);
197
        }
198
199 10
        $this->controllerHelper->persistEntity($entity);
200
201 10
        $this->controllerHelper->dispatchEvent("symfony_generics.entity_interaction", new EntityInteractionEvent(
202 10
            $this->entityClass,
203 10
            null, # TODO: get id via reflection
204 10
            $entity,
205 10
            "__construct",
206 10
            $constructArguments
207
        ));
208
209 10
        $this->controllerHelper->flushORM();
210
211 10
        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 9
        return new Response($this->successResponse, 200);
231
    }
232
233
    /**
234
     * @param object $factoryObject
235
     */
236 16
    private function findConstructorReflection(&$factoryObject = null): ?ReflectionFunctionAbstract
237
    {
238
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
239 16
        $constructorReflection = null;
240
241 16
        if (!empty($this->factory)) {
242 10
            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 3
            } elseif (method_exists($this->entityClass, $this->factory)) {
272
                # Create by static factory method on entity class
273
274 2
                $constructorReflection = (new ReflectionClass($this->entityClass))->getMethod($this->factory);
275
276 1
            } elseif (function_exists($this->factory)) {
277
                # Create by factory function
278
279 6
                $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 12
        return $constructorReflection;
289
    }
290
291
    /**
292
     * @param object|null $factoryObject
293
     *
294
     * @return object
295
     */
296 11
    private function createEntityByConstructor(
297
        ReflectionFunctionAbstract $constructorReflection,
298
        array $constructArguments,
299
        $factoryObject
300
    ) {
301
        /** @var object|null $entity */
302 11
        $entity = null;
303
304 11
        if ($constructorReflection instanceof ReflectionMethod) {
305 10
            if ($constructorReflection->isConstructor()) {
306 5
                $entity = $constructorReflection->getDeclaringClass()->newInstanceArgs($constructArguments);
307
308 5
            } elseif ($constructorReflection->isStatic()) {
309 2
                $entity = $constructorReflection->invokeArgs(null, $constructArguments);
310
311
            } else {
312 10
                $entity = $constructorReflection->invokeArgs($factoryObject, $constructArguments);
313
            }
314
315 1
        } elseif ($constructorReflection instanceof ReflectionFunction) {
316 1
            $entity = $constructorReflection->invokeArgs($constructArguments);
317
        }
318
319 11
        Assert::isInstanceOf($entity, $this->entityClass);
320
321 10
        return $entity;
322
    }
323
324
    /**
325
     * @param object $entity
326
     */
327 11
    private function performPostCreationCalls($entity, Request $request): void
328
    {
329 11
        $classReflection = new ReflectionClass($this->entityClass);
330
331 11
        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 11
    }
346
347
}
348