Passed
Push — master ( 3f1e18...b03604 )
by Gerrit
05:04
created

createEntityByConstructor()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.3906

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 9
cts 12
cp 0.75
rs 9.1768
c 0
b 0
f 0
cc 5
nc 5
nop 3
crap 5.3906
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 15
    public function __construct(
99
        ControllerHelperInterface $controllerHelper,
100
        ArgumentCompilerInterface $argumentBuilder,
101
        ContainerInterface $container,
102
        array $options
103
    ) {
104 15
        Assert::null($this->controllerHelper);
105 15
        Assert::keyExists($options, 'entity-class');
106
107 14
        $options = array_merge([
108 14
            '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 14
        ], $options);
117
118 14
        $this->controllerHelper = $controllerHelper;
119 14
        $this->argumentBuilder = $argumentBuilder;
120 14
        $this->container = $container;
121 14
        $this->entityClass = $options['entity-class'];
122 14
        $this->successResponse = $options['success-response'];
123 14
        $this->factory = $options['factory'];
124 14
        $this->authorizationAttribute = $options['authorization-attribute'];
125 14
        $this->constructArguments = $options['arguments'];
126 14
        $this->successRedirectRoute = $options['success-redirect'];
127 14
        $this->successRedirectArguments = $options['success-redirect-arguments'];
128 14
        $this->successRedirectStatus = (int)$options['success-redirect-status'];
129
130 14
        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 11
    }
139
140 10
    public function createEntity(Request $request): Response
141
    {
142
        /** @var object|null $factoryObject */
143 10
        $factoryObject = null;
144
145 10
        if (!empty($this->authorizationAttribute)) {
146 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $request);
147
        }
148
149
        /** @var ReflectionFunctionAbstract $constructorReflection */
150 10
        $constructorReflection = $this->findConstructorReflection($factoryObject);
151
152
        /** @var array<int, mixed> $constructArguments */
153 6
        $constructArguments = $this->argumentBuilder->buildCallArguments(
154 6
            $constructorReflection,
155 6
            $this->constructArguments,
156 6
            $request
157
        );
158
159
        /** @var object $entity */
160 6
        $entity = $this->createEntityByConstructor($constructorReflection, $constructArguments, $factoryObject);
161
162 5
        $this->performPostCreationCalls($entity, $request);
163
164 5
        if (!empty($this->authorizationAttribute)) {
165 1
            $this->controllerHelper->denyAccessUnlessGranted($this->authorizationAttribute, $entity);
166
        }
167
168 4
        $this->controllerHelper->persistEntity($entity);
169
170 4
        $this->controllerHelper->dispatchEvent("symfony_generics.entity_interaction", new EntityInteractionEvent(
171 4
            $this->entityClass,
172 4
            null, # TODO: get id via reflection
173 4
            $entity,
174 4
            "__construct",
175 4
            $constructArguments
176
        ));
177
178 4
        $this->controllerHelper->flushORM();
179
180 4
        if (!empty($this->successRedirectRoute)) {
181
            /** @var array $redirectArguments */
182
            $redirectArguments = $this->argumentBuilder->buildArguments(
183
                $this->successRedirectArguments,
184
                $request
185
            );
186
            $redirectArguments['entityId'] = $entity->getId(); # TODO: getId might not always exist! get id via doctrine
187
188
            return $this->controllerHelper->redirectToRoute(
189
                $this->successRedirectRoute,
190
                $redirectArguments,
191
                $this->successRedirectStatus
192
            );
193
        }
194
195 4
        return new Response($this->successResponse, 200);
196
    }
197
198
    /**
199
     * @param object $factoryObject
200
     */
201 10
    private function findConstructorReflection(&$factoryObject = null): ReflectionFunctionAbstract
202
    {
203
        /** @var ReflectionFunctionAbstract|null $constructorReflection */
204 10
        $constructorReflection = null;
205
206 10
        if (!empty($this->factory)) {
207 7
            if (is_int(strpos($this->factory, '::'))) {
208 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...
209
210 7
                if (!empty($factoryClass)) {
211 6
                    if ($factoryClass[0] == '@') {
212
                        # Create by factory-service-object
213
214 6
                        $factoryObject = $this->container->get(substr($factoryClass, 1));
215
216 6
                        Assert::object($factoryObject, sprintf(
217 6
                            "Did not find service '%s'!",
218 6
                            substr($factoryClass, 1)
219
                        ));
220
221 5
                        $constructorReflection = (new ReflectionObject($factoryObject))->getMethod($factoryMethod);
222
223
                    } else {
224
                        # Create by static factory-method of other class
225
226 3
                        $constructorReflection = (new ReflectionClass($factoryClass))->getMethod($factoryMethod);
227
                    }
228
229
                } else {
230 1
                    throw new ErrorException(sprintf(
231 1
                        "Invalid constructor definition: '%s'!",
232 4
                        $this->factory
233
                    ));
234
                }
235
236
            } elseif (method_exists($this->entityClass, $this->factory)) {
237
                # Create by static factory method on entity class
238
239
                $constructorReflection = (new ReflectionClass($this->entityClass))->getMethod($this->factory);
240
241
            } elseif (function_exists($this->factory)) {
242
                # Create by factory function
243
244 3
                $constructorReflection = new ReflectionFunction($this->factory);
245
            }
246
247
        } else {
248
            # Create by calling the constructor directly
249
250 3
            $constructorReflection = (new ReflectionClass($this->entityClass))->getConstructor();
251
        }
252
253 6
        return $constructorReflection;
254
    }
255
256
    /**
257
     * @param object|null $factoryObject
258
     *
259
     * @return object
260
     */
261 6
    private function createEntityByConstructor(
262
        ReflectionFunctionAbstract $constructorReflection,
263
        array $constructArguments,
264
        $factoryObject
265
    ) {
266
        /** @var object|null $entity */
267 6
        $entity = null;
268
269 6
        if ($constructorReflection instanceof ReflectionMethod) {
270 6
            if ($constructorReflection->isConstructor()) {
271 3
                $entity = $constructorReflection->getDeclaringClass()->newInstanceArgs($constructArguments);
272
273 3
            } elseif ($constructorReflection->isStatic()) {
274
                $entity = $constructorReflection->invokeArgs(null, $constructArguments);
275
276
            } else {
277 6
                $entity = $constructorReflection->invokeArgs($factoryObject, $constructArguments);
278
            }
279
280
        } elseif ($constructorReflection instanceof ReflectionFunction) {
281
            $entity = $constructorReflection->invokeArgs($constructArguments);
282
        }
283
284 6
        Assert::isInstanceOf($entity, $this->entityClass);
285
286 5
        return $entity;
287
    }
288
289
    /**
290
     * @param object $entity
291
     */
292 5
    private function performPostCreationCalls($entity, Request $request): void
293
    {
294 5
        $classReflection = new ReflectionClass($this->entityClass);
295
296 5
        foreach ($this->calls as $methodName => $callArgumentConfiguration) {
297
            /** @var array $callArgumentConfiguration */
298
299
            /** @var ReflectionMethod $methodReflection */
300 1
            $methodReflection = $classReflection->getMethod($methodName);
301
302 1
            $callArguments = $this->argumentBuilder->buildCallArguments(
303 1
                $methodReflection,
304 1
                $callArgumentConfiguration,
305 1
                $request
306
            );
307
308 1
            $methodReflection->invokeArgs($entity, $callArguments);
309
        }
310 5
    }
311
312
}
313