Passed
Push — master ( 3624d1...36fc6d )
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 21
    public function __construct(
103
        ControllerHelperInterface $controllerHelper,
104
        ArgumentCompilerInterface $argumentBuilder,
105
        ContainerInterface $container,
106
        array $options
107
    ) {
108 21
        Assert::null($this->controllerHelper);
109 21
        Assert::keyExists($options, 'entity-class');
110
111
        /** @var int $defaultRedirectStatus */
112 20
        $defaultRedirectStatus = 303;
113
114 20
        $options = array_merge([
115 20
            'calls' => [],
116 20
            'success-response' => "object created",
117
            'factory' => null,
118
            'authorization-attribute' => null,
119
            'arguments' => [],
120
            'success-redirect' => null,
121
            'success-redirect-arguments' => [],
122 20
            'success-redirect-status' => $defaultRedirectStatus,
123 20
            'entity-id-getter' => 'getId',
124 20
            'entity-id-key' => 'entityId',
125 20
        ], $options);
126
127 20
        $this->controllerHelper = $controllerHelper;
128 20
        $this->argumentBuilder = $argumentBuilder;
129 20
        $this->container = $container;
130 20
        $this->entityClass = $options['entity-class'];
131 20
        $this->entityIdGetter = $options['entity-id-getter'];
132 20
        $this->entityIdKey = $options['entity-id-key'];
133 20
        $this->successResponse = $options['success-response'];
134 20
        $this->factory = $options['factory'];
135 20
        $this->authorizationAttribute = $options['authorization-attribute'];
136 20
        $this->constructArguments = $options['arguments'];
137 20
        $this->successRedirectRoute = $options['success-redirect'];
138 20
        $this->successRedirectArguments = $options['success-redirect-arguments'];
139 20
        $this->successRedirectStatus = $options['success-redirect-status'];
140
141 20
        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 17
    }
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 15
    public function createEntity(Request $request): Response
162
    {
163
        /** @var object|null $factoryObject */
164 15
        $factoryObject = null;
165
166 15
        if (!empty($this->authorizationAttribute)) {
167 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $request);
168
        }
169
170
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
171 15
        $constructorReflection = $this->findConstructorReflection($factoryObject);
172
173
        /** @var array<int, mixed> $constructArguments */
174 11
        $constructArguments = array();
175
176 11
        if ($constructorReflection instanceof ReflectionFunctionAbstract) {
177 10
            $constructArguments = $this->argumentBuilder->buildCallArguments(
178 10
                $constructorReflection,
179 10
                $this->constructArguments,
180 10
                $request
181
            );
182
183
            /** @var object $entity */
184 10
            $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 10
        $this->performPostCreationCalls($entity, $request);
194
195 10
        if (!empty($this->authorizationAttribute)) {
196 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $entity);
197
        }
198
199 9
        $this->controllerHelper->persistEntity($entity);
200
201 9
        $this->controllerHelper->dispatchEvent("symfony_generics.entity_interaction", new EntityInteractionEvent(
202 9
            $this->entityClass,
203 9
            null, # TODO: get id via reflection
204 9
            $entity,
205 9
            "__construct",
206 9
            $constructArguments
207
        ));
208
209 9
        $this->controllerHelper->flushORM();
210
211 9
        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 8
        return new Response($this->successResponse, 200);
231
    }
232
233
    /**
234
     * @param object $factoryObject
235
     */
236 15
    private function findConstructorReflection(&$factoryObject = null): ?ReflectionFunctionAbstract
237
    {
238
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
239 15
        $constructorReflection = null;
240
241 15
        if (!empty($this->factory)) {
242 9
            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 2
            } 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
            } elseif (function_exists($this->factory)) {
277
                # Create by factory function
278
279 5
                $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 11
        return $constructorReflection;
289
    }
290
291
    /**
292
     * @param object|null $factoryObject
293
     *
294
     * @return object
295
     */
296 10
    private function createEntityByConstructor(
297
        ReflectionFunctionAbstract $constructorReflection,
298
        array $constructArguments,
299
        $factoryObject
300
    ) {
301
        /** @var object|null $entity */
302 10
        $entity = null;
303
304 10
        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
        } elseif ($constructorReflection instanceof ReflectionFunction) {
316
            $entity = $constructorReflection->invokeArgs($constructArguments);
317
        }
318
319 10
        Assert::isInstanceOf($entity, $this->entityClass);
320
321 9
        return $entity;
322
    }
323
324
    /**
325
     * @param object $entity
326
     */
327 10
    private function performPostCreationCalls($entity, Request $request): void
328
    {
329 10
        $classReflection = new ReflectionClass($this->entityClass);
330
331 10
        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 10
    }
346
347
}
348