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