Passed
Push — master ( d0187e...7d6d5a )
by Gerrit
62:29
created

ArgumentCompiler   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 78.4%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 4
dl 0
loc 295
ccs 98
cts 125
cp 0.784
rs 9.6
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A buildArguments() 0 31 3
C buildCallArguments() 0 80 12
B resolveArgumentConfiguration() 0 66 9
C resolveStringArgumentConfiguration() 0 84 10
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\Services;
14
15
use Addiks\SymfonyGenerics\Services\ArgumentCompilerInterface;
16
use Psr\Container\ContainerInterface;
17
use ErrorException;
18
use ReflectionParameter;
19
use ReflectionType;
20
use ReflectionMethod;
21
use Symfony\Component\HttpFoundation\Request;
22
use Webmozart\Assert\Assert;
23
use ReflectionClass;
24
use ReflectionFunctionAbstract;
25
use InvalidArgumentException;
26
use ReflectionException;
27
use Doctrine\ORM\EntityManagerInterface;
28
use ValueObjects\ValueObjectInterface;
29
30
final class ArgumentCompiler implements ArgumentCompilerInterface
31
{
32
33
    /**
34
     * @var ContainerInterface
35
     */
36
    private $container;
37
38
    /**
39
     * @var EntityManagerInterface
40
     */
41
    private $entityManager;
42
43 5
    public function __construct(
44
        ContainerInterface $container,
45
        EntityManagerInterface $entityManager
46
    ) {
47 5
        $this->container = $container;
48 5
        $this->entityManager = $entityManager;
49 5
    }
50
51 2
    public function buildArguments(
52
        array $argumentsConfiguration,
53
        Request $request,
54
        array $additionalData = array()
55
    ): array {
56
        /** @var array<int, mixed> $routeArguments */
57 2
        $routeArguments = array();
58
59 2
        foreach ($argumentsConfiguration as $key => $argumentConfiguration) {
60
            /** @var array|string $argumentConfiguration */
61
62
            /** @var string|null $parameterTypeName */
63 2
            $parameterTypeName = null;
64
65 2
            if (isset($argumentConfiguration['entity-class'])) {
66
                $parameterTypeName = $argumentConfiguration['entity-class'];
67
            }
68
69
            /** @var mixed $argumentValue */
70 2
            $argumentValue = $this->resolveArgumentConfiguration(
71 2
                $argumentConfiguration,
72 2
                $request,
73 2
                $parameterTypeName,
74 2
                $additionalData
75
            );
76
77 2
            $routeArguments[$key] = $argumentValue;
78
        }
79
80 1
        return $routeArguments;
81
    }
82
83 4
    public function buildCallArguments(
84
        ReflectionFunctionAbstract $routineReflection,
85
        array $argumentsConfiguration,
86
        Request $request,
87
        array $additionalData = array()
88
    ): array {
89
        /** @var array<int, mixed> $callArguments */
90 4
        $callArguments = array();
91
92 4
        foreach ($routineReflection->getParameters() as $index => $parameterReflection) {
93
            /** @var ReflectionParameter $parameterReflection */
94
95
            /** @var string $parameterName */
96 3
            $parameterName = $parameterReflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $parameterReflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
97
98
            /** @var mixed $requestValue */
99 3
            $requestValue = $request->get($parameterName);
100
101
            /** @var string|null $parameterTypeName */
102 3
            $parameterTypeName = null;
103
104 3
            if ($parameterReflection->hasType()) {
105
                /** @var ReflectionType|null $parameterType */
106 2
                $parameterType = $parameterReflection->getType();
107
108 2
                if ($parameterType instanceof ReflectionType) {
0 ignored issues
show
Bug introduced by
The class ReflectionType does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
109 2
                    $parameterTypeName = $parameterType->__toString();
110
                }
111
            }
112
113 3
            if (isset($argumentsConfiguration[$parameterName])) {
114
                /** @var array|string $argumentConfiguration */
115 3
                $argumentConfiguration = $argumentsConfiguration[$parameterName];
116
117
                /** @var mixed $argumentValue */
118 3
                $argumentValue = $argumentConfiguration;
119
120 3
                if (is_string($argumentConfiguration) || is_array($argumentConfiguration)) {
121
                    /** @var mixed $argumentValue */
122 3
                    $argumentValue = $this->resolveArgumentConfiguration(
123 3
                        $argumentConfiguration,
124 3
                        $request,
125 3
                        $parameterTypeName,
126 3
                        $additionalData
127
                    );
128
                }
129
130 1
                $callArguments[$index] = $argumentValue;
131
132 2
            } elseif (!is_null($requestValue)) {
133 1
                if (!is_null($parameterTypeName)) {
134
                    /** @psalm-suppress UndefinedClass ValueObjects\ValueObjectInterface does not exist */
135
                    if (is_subclass_of($parameterTypeName, ValueObjectInterface::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \ValueObjects\ValueObjectInterface::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
136
                        $argumentValue = $parameterTypeName::fromNative($requestValue);
137
138
                        $callArguments[$index] = $argumentValue;
139
                    }
140
141
                } else {
142 1
                    $callArguments[$index] = $requestValue;
143
                }
144
145 1
            } elseif ($parameterTypeName === Request::class) {
146
                $callArguments[$index] = $request;
147
148
            } else {
149
                try {
150 1
                    $callArguments[$index] = $parameterReflection->getDefaultValue();
151
152 1
                } catch (ReflectionException $exception) {
153 1
                    throw new InvalidArgumentException(sprintf(
154 1
                        "Missing argument '%s' for this call!",
155 2
                        $parameterName
156
                    ));
157
                }
158
            }
159
        }
160
161 2
        return $callArguments;
162
    }
163
164
    /**
165
     * @param array|string $argumentConfiguration
166
     *
167
     * @return mixed
168
     */
169 5
    private function resolveArgumentConfiguration(
170
        $argumentConfiguration,
171
        Request $request,
172
        ?string $parameterTypeName,
173
        array $additionalData = array()
174
    ) {
175
        /** @var mixed $argumentValue */
176 5
        $argumentValue = null;
177
178 5
        if (is_array($argumentConfiguration)) {
179 3
            if (!empty($parameterTypeName) && isset($argumentConfiguration['entity-id'])) {
180
                /** @var string $entityId */
181
                $entityId = $argumentConfiguration['entity-id'];
182
                $entityId = $this->resolveStringArgumentConfiguration(
183
                    $entityId,
184
                    $request,
185
                    $additionalData
186
                );
187
188
                $argumentValue = $this->entityManager->find(
189
                    $parameterTypeName,
190
                    $entityId
191
                );
192
193 3
            } elseif (isset($argumentConfiguration['id'])) {
194 3
                $argumentValue = $this->container->get($argumentConfiguration['id']);
195 3
                Assert::object($argumentValue, sprintf(
196 3
                    "Did not find service '%s'!",
197 3
                    $argumentConfiguration['id']
198
                ));
199
            }
200
201 2
            if (isset($argumentConfiguration['method'])) {
202 2
                $methodReflection = new ReflectionMethod($argumentValue, $argumentConfiguration['method']);
203
204 2
                if (!isset($argumentConfiguration['arguments'])) {
205 1
                    $argumentConfiguration['arguments'] = [];
206
                }
207
208
                /** @var array $callArguments */
209 2
                $callArguments = $this->buildCallArguments(
210 2
                    $methodReflection,
211 2
                    $argumentConfiguration['arguments'],
212 2
                    $request
213
                );
214
215 1
                $argumentValue = $methodReflection->invokeArgs($argumentValue, $callArguments);
216
            }
217
218
        } else {
219 3
            $argumentValue = $this->resolveStringArgumentConfiguration(
220 3
                $argumentConfiguration,
221 3
                $request,
222 3
                $additionalData
223
            );
224
        }
225
226 3
        if (!empty($parameterTypeName)) {
227 1
            if (class_exists($parameterTypeName)) {
228 1
                $argumentValue = $this->entityManager->find($parameterTypeName, $argumentValue);
229
                # TODO: error handling "not an entty", "entity not found", ...
230
            }
231
        }
232
233 3
        return $argumentValue;
234
    }
235
236
    /**
237
     * @return mixed
238
     */
239 3
    private function resolveStringArgumentConfiguration(
240
        string $argumentConfiguration,
241
        Request $request,
242
        array $additionalData = array()
243
    ) {
244
        /** @var mixed $argumentValue */
245 3
        $argumentValue = null;
246
247 3
        if (is_int(strpos($argumentConfiguration, '::'))) {
248 2
            [$factoryClass, $factoryMethod] = explode('::', $argumentConfiguration);
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...
249
250 2
            if (!empty($factoryClass)) {
251 2
                if ($factoryClass[0] == '@') {
252
                    /** @var string $factoryServiceId */
253 2
                    $factoryServiceId = substr($factoryClass, 1);
254
255
                    /** @var object|null $factoryObject */
256 2
                    $factoryObject = $this->container->get($factoryServiceId);
257
258 2
                    Assert::methodExists($factoryObject, $factoryMethod, sprintf(
259 2
                        "Did not find service with id '%s' that has a method '%s'!",
260 2
                        $factoryServiceId,
261 2
                        $factoryMethod
262
                    ));
263
264 1
                    $factoryReflection = new ReflectionClass($factoryObject);
265
266
                    /** @var ReflectionMethod $methodReflection */
267 1
                    $methodReflection = $factoryReflection->getMethod($factoryMethod);
268
269 1
                    $callArguments = $this->buildCallArguments(
270 1
                        $methodReflection,
271 1
                        [], # TODO
272 1
                        $request
273
                    );
274
275
                    # Create by factory-service-object
276 1
                    $argumentValue = call_user_func_array([$factoryObject, $factoryMethod], $callArguments);
277
278
                } else {
279
                    # Create by static factory-method of other class
280 1
                    $argumentValue = call_user_func_array($argumentConfiguration, []);
281
                }
282
283 1
            } else {
284
                # TODO: What to do here? What could "::Something" be? A template?
285
            }
286
287 3
        } elseif ($argumentConfiguration[0] == '$') {
288 3
            $argumentValue = $request->get(substr($argumentConfiguration, 1));
289
290 1
        } elseif ($argumentConfiguration[0] == '@') {
291 1
            $argumentValue = $this->container->get(substr($argumentConfiguration, 1));
292
293
        } elseif ($argumentConfiguration[0] == '%') {
294
            /** @var string $key */
295
            $key = substr($argumentConfiguration, 1);
296
297
            if (is_int(strpos($key, '.'))) {
298
                [$key, $property] = explode('.', $key);
0 ignored issues
show
Bug introduced by
The variable $property 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...
299
300
                Assert::keyExists($additionalData, $key, sprintf(
301
                    'Missing additional-data key "%s"',
302
                    $key
303
                ));
304
305
                $argumentValue = $additionalData[$key];
306
307
                if (is_object($argumentValue) && method_exists($argumentValue, $property)) {
308
                    $argumentValue = call_user_func([$argumentValue, $property]);
309
                }
310
311
            } else {
312
                Assert::keyExists($additionalData, $key, sprintf(
313
                    'Missing additional-data key "%s"',
314
                    $key
315
                ));
316
317
                $argumentValue = $additionalData[$key];
318
            }
319
        }
320
321 3
        return $argumentValue;
322
    }
323
324
}
325