Test Setup Failed
Push — master ( e2ccdd...da0315 )
by Gerrit
09:14
created

GenericEntityCreateController   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 316
ccs 110
cts 110
cp 1
rs 10
c 0
b 0
f 0
wmc 23
lcom 1
cbo 6
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
    private ControllerHelperInterface $controllerHelper;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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