Completed
Push — master ( 677e94...463d73 )
by Gerrit
02:18
created

ArgumentCompiler::buildArguments()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.004

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 12
cts 13
cp 0.9231
rs 9.424
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 3.004
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 ReflectionParameter;
18
use ReflectionType;
19
use ReflectionMethod;
20
use Symfony\Component\HttpFoundation\Request;
21
use Webmozart\Assert\Assert;
22
use ReflectionClass;
23
use ReflectionFunctionAbstract;
24
use InvalidArgumentException;
25
use ReflectionException;
26
use Doctrine\ORM\EntityManagerInterface;
27
use ValueObjects\ValueObjectInterface;
28
use Symfony\Component\HttpFoundation\FileBag;
29
use Symfony\Component\HttpFoundation\File\UploadedFile;
30
31
final class ArgumentCompiler implements ArgumentCompilerInterface
32
{
33
34
    /**
35
     * @var ContainerInterface
36
     */
37
    private $container;
38
39
    /**
40
     * @var EntityManagerInterface
41
     */
42
    private $entityManager;
43
44 12
    public function __construct(
45
        ContainerInterface $container,
46
        EntityManagerInterface $entityManager
47
    ) {
48 12
        $this->container = $container;
49 12
        $this->entityManager = $entityManager;
50 12
    }
51
52 9
    public function buildArguments(
53
        array $argumentsConfiguration,
54
        Request $request,
55
        array $additionalData = array()
56
    ): array {
57
        /** @var array<int, mixed> $routeArguments */
58 9
        $routeArguments = array();
59
60 9
        foreach ($argumentsConfiguration as $key => $argumentConfiguration) {
61
            /** @var array|string $argumentConfiguration */
62
63
            /** @var string|null $parameterTypeName */
64 9
            $parameterTypeName = null;
65
66 9
            if (isset($argumentConfiguration['entity-class'])) {
67
                $parameterTypeName = $argumentConfiguration['entity-class'];
68
            }
69
70
            /** @var mixed $argumentValue */
71 9
            $argumentValue = $this->resolveArgumentConfiguration(
72 9
                $argumentConfiguration,
73 9
                $request,
74 9
                $parameterTypeName,
75 9
                $additionalData
76
            );
77
78 6
            $routeArguments[$key] = $argumentValue;
79
        }
80
81 5
        return $routeArguments;
82
    }
83
84 4
    public function buildCallArguments(
85
        ReflectionFunctionAbstract $routineReflection,
86
        array $argumentsConfiguration,
87
        Request $request,
88
        array $predefinedArguments = array(),
89
        array $additionalData = array()
90
    ): array {
91
        /** @var array<int, mixed> $callArguments */
92 4
        $callArguments = array();
93
94 4
        foreach ($routineReflection->getParameters() as $index => $parameterReflection) {
95
            /** @var ReflectionParameter $parameterReflection */
96
97 3
            if (isset($predefinedArguments[$index])) {
98
                $callArguments[] = $predefinedArguments[$index];
99
                continue;
100
            }
101
102
            /** @var string $parameterName */
103 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...
104
105
            /** @var mixed $requestValue */
106 3
            $requestValue = $request->get($parameterName);
107
108
            /** @var string|null $parameterTypeName */
109 3
            $parameterTypeName = null;
110
111 3
            if ($parameterReflection->hasType()) {
112
                /** @var ReflectionType|null $parameterType */
113 2
                $parameterType = $parameterReflection->getType();
114
115 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...
116 2
                    $parameterTypeName = $parameterType->__toString();
117
                }
118
            }
119
120 3
            if (isset($argumentsConfiguration[$parameterName])) {
121
                /** @var array|string $argumentConfiguration */
122 3
                $argumentConfiguration = $argumentsConfiguration[$parameterName];
123
124
                /** @var mixed $argumentValue */
125 3
                $argumentValue = $argumentConfiguration;
126
127 3
                if (is_string($argumentConfiguration) || is_array($argumentConfiguration)) {
128 3
                    $argumentValue = $this->resolveArgumentConfiguration(
129 3
                        $argumentConfiguration,
130 3
                        $request,
131 3
                        $parameterTypeName,
132 3
                        $additionalData
133
                    );
134
                }
135
136 1
                $callArguments[$index] = $argumentValue;
137
138 2
            } elseif (isset($argumentsConfiguration[$index])) {
139
                /** @var array|string $argumentConfiguration */
140
                $argumentConfiguration = $argumentsConfiguration[$index];
141
142
                /** @var mixed $argumentValue */
143
                $argumentValue = $argumentConfiguration;
144
145
                if (is_string($argumentConfiguration) || is_array($argumentConfiguration)) {
146
                    $argumentValue = $this->resolveArgumentConfiguration(
147
                        $argumentConfiguration,
148
                        $request,
149
                        $parameterTypeName,
150
                        $additionalData
151
                    );
152
                }
153
154
                $callArguments[$index] = $argumentValue;
155
156 2
            } elseif (!is_null($requestValue)) {
157 1
                if (!is_null($parameterTypeName)) {
158
                    /** @psalm-suppress UndefinedClass ValueObjects\ValueObjectInterface does not exist */
159
                    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...
160
                        $argumentValue = $parameterTypeName::fromNative($requestValue);
161
162
                        $callArguments[$index] = $argumentValue;
163
                    }
164
165
                } else {
166 1
                    $callArguments[$index] = $requestValue;
167
                }
168
169 1
            } elseif ($parameterTypeName === Request::class) {
170
                $callArguments[$index] = $request;
171
172
            } else {
173
                try {
174 1
                    $callArguments[$index] = $parameterReflection->getDefaultValue();
175
176 1
                } catch (ReflectionException $exception) {
177 1
                    throw new InvalidArgumentException(sprintf(
178 1
                        "Missing argument '%s' for the call to '%s'!",
179 1
                        $parameterName,
180 2
                        $routineReflection->getName()
181
                    ));
182
                }
183
            }
184
        }
185
186 2
        return $callArguments;
187
    }
188
189
    /**
190
     * @param array|string $argumentConfiguration
191
     *
192
     * @return mixed
193
     */
194 12
    public function resolveArgumentConfiguration(
195
        $argumentConfiguration,
196
        Request $request,
197
        ?string $parameterTypeName,
198
        array $additionalData = array()
199
    ) {
200
        /** @var mixed $argumentValue */
201 12
        $argumentValue = null;
202
203 12
        if (is_array($argumentConfiguration)) {
204 3
            if (isset($argumentConfiguration['entity-class'])) {
205
                $parameterTypeName = $argumentConfiguration['entity-class'];
206
            }
207
208 3
            if (isset($argumentConfiguration['service-id'])) {
209 3
                $argumentValue = $this->container->get($argumentConfiguration['service-id']);
210
211 3
                Assert::object($argumentValue, sprintf(
212 3
                    "Did not find service '%s'!",
213 3
                    $argumentConfiguration['service-id']
214
                ));
215
216
            } elseif (isset($argumentConfiguration['entity-id'])) {
217
                $argumentValue = $this->resolveStringArgumentConfiguration(
218
                    $argumentConfiguration['entity-id'],
219
                    $request
220
                );
221
222
            } elseif (class_exists($parameterTypeName)) {
223
                $argumentValue = $this->resolveStringArgumentConfiguration(
224
                    $parameterTypeName,
225
                    $request
226
                );
227
            }
228
229 2
            if (isset($argumentConfiguration['method'])) {
230 2
                $methodReflection = new ReflectionMethod($argumentValue, $argumentConfiguration['method']);
231
232 2
                if (!isset($argumentConfiguration['arguments'])) {
233 1
                    $argumentConfiguration['arguments'] = [];
234
                }
235
236
                /** @var array $callArguments */
237 2
                $callArguments = $this->buildCallArguments(
238 2
                    $methodReflection,
239 2
                    $argumentConfiguration['arguments'],
240 2
                    $request
241
                );
242
243 1
                $argumentValue = $methodReflection->invokeArgs($argumentValue, $callArguments);
244
            }
245
246
        } else {
247 10
            $argumentValue = $this->resolveStringArgumentConfiguration(
248 10
                $argumentConfiguration,
249 10
                $request,
250 10
                $additionalData
251
            );
252
        }
253
254 7
        if (!empty($parameterTypeName) && !is_object($argumentValue)) {
255 1
            if (class_exists($parameterTypeName)) {
256
                /** @psalm-suppress UndefinedClass ValueObjects\ValueObjectInterface does not exist */
257 1
                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...
258
                    $argumentValue = call_user_func("{$parameterTypeName}::fromNative", $argumentValue);
259
260
                } else {
261 1
                    $argumentValue = $this->entityManager->find($parameterTypeName, $argumentValue);
262
                }
263
            }
264
        }
265
266 7
        return $argumentValue;
267
    }
268
269
    /**
270
     * @return mixed
271
     */
272 10
    private function resolveStringArgumentConfiguration(
273
        string $argumentConfiguration,
274
        Request $request,
275
        array $additionalData = array()
276
    ) {
277
        /** @var mixed $argumentValue */
278 10
        $argumentValue = null;
279
280 10
        $argumentConfiguration = trim($argumentConfiguration);
281
282 10
        if (is_int(strpos($argumentConfiguration, '::'))) {
283 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 seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
284
285
            /** @var array<string> $callArgumentConfigurations */
286 2
            $callArgumentConfigurations = array();
287
288 2
            if (is_int(strpos($factoryMethod, '('))) {
0 ignored issues
show
Bug introduced by
The variable $factoryMethod seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
289
                $factoryMethod = str_replace(')', '', $factoryMethod);
0 ignored issues
show
Bug introduced by
The variable $factoryMethod seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
290
                [$factoryMethod, $rawArguments] = explode('(', $factoryMethod, 2);
0 ignored issues
show
Bug introduced by
The variable $rawArguments 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...
291
292
                foreach (explode(",", $rawArguments) as $rawArgument) {
293
                    /** @var string $rawArgument */
294
295
                    $callArgumentConfigurations[] = trim($rawArgument);
296
                }
297
            }
298
299 2
            if (!empty($factoryClass)) {
300 2
                if ($factoryClass[0] == '@') {
301
                    /** @var string $factoryServiceId */
302 2
                    $factoryServiceId = substr($factoryClass, 1);
303
304
                    /** @var object $factoryObject */
305 2
                    $factoryObject = $this->container->get($factoryServiceId);
306
307 2
                    Assert::object($factoryObject, sprintf(
308 2
                        "Could not find service with id '%s'!",
309 2
                        $factoryServiceId
310
                    ));
311
312 2
                    Assert::methodExists($factoryObject, $factoryMethod, sprintf(
0 ignored issues
show
Bug introduced by
The variable $factoryMethod does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
313 2
                        "Method '%s' does not exist on service '%s'! (Class '%s')",
314 2
                        $factoryMethod,
315 2
                        $factoryServiceId,
316 2
                        get_class($factoryObject)
317
                    ));
318
319
                    /** @var array<int, mixed> $callArguments */
320 1
                    $callArguments = $this->buildCallArguments(
321 1
                        new ReflectionMethod($factoryObject, $factoryMethod),
322 1
                        $callArgumentConfigurations,
323 1
                        $request
324
                    );
325
326 1
                    $argumentValue = $this->callOnObject(
327 1
                        $factoryObject,
328 1
                        $factoryMethod,
329 1
                        $callArguments,
330 1
                        $request,
331 1
                        sprintf(
332 1
                            "Did not find service with id '%s' that has a method '%s'!",
333 1
                            $factoryServiceId,
334 1
                            $factoryMethod
335
                        )
336
                    );
337
338
                } elseif (is_int(strpos($factoryClass, '#'))) {
339
                    # Create by call on entity
340
                    [$entityClass, $idRaw] = explode('#', $factoryClass);
0 ignored issues
show
Bug introduced by
The variable $entityClass 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 $idRaw 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...
341
342
                    $id = $this->resolveStringArgumentConfiguration($idRaw, $request, $additionalData);
343
344
                    /** @var object $entity */
345
                    $entity = $this->entityManager->find($entityClass, $id);
346
347
                    Assert::object($entity, sprintf(
348
                        "Could not find entity '%s' with id '%s'!",
349
                        $entityClass,
350
                        $id
351
                    ));
352
353
                    /** @var array<int, mixed> $callArguments */
354
                    $callArguments = $this->buildCallArguments(
355
                        new ReflectionMethod($entity, $factoryMethod),
356
                        $callArgumentConfigurations,
357
                        $request
358
                    );
359
360
                    $argumentValue = $this->callOnObject(
361
                        $entity,
362
                        $factoryMethod,
363
                        $callArguments,
364
                        $request,
365
                        sprintf(
366
                            "Entity '%s' does not have method '%s'!",
367
                            $entityClass,
368
                            $factoryMethod
369
                        )
370
                    );
371
372
                } else {
373
                    $callArguments = array();
374
375
                    if (is_int(strpos($argumentConfiguration, '('))) {
376
                        $argumentConfiguration = str_replace(")", "", $argumentConfiguration);
377
                        [$argumentConfiguration, $callArgumentsRaw] = explode('(', $argumentConfiguration);
0 ignored issues
show
Bug introduced by
The variable $callArgumentsRaw does not exist. Did you mean $callArguments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
378
379
                        if (!empty($callArgumentsRaw)) {
0 ignored issues
show
Bug introduced by
The variable $callArgumentsRaw does not exist. Did you mean $callArguments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
380
                            foreach (explode(',', $callArgumentsRaw) as $callArgumentRaw) {
381
                                $callArguments[] = $this->resolveStringArgumentConfiguration(
382
                                    $callArgumentRaw,
383
                                    $request,
384
                                    $additionalData
385
                                );
386
                            }
387
                        }
388
                    }
389
390
                    # Create by static factory-method of other class
391 1
                    $argumentValue = call_user_func_array($argumentConfiguration, $callArguments);
392
                }
393
394 1
            } else {
395
                # TODO: What to do here? What could "::Something" be? A template?
396
            }
397
398 10
        } elseif ($argumentConfiguration[0] == "'" && $argumentConfiguration[strlen($argumentConfiguration) - 1] == "'") {
399 1
            $argumentValue = substr($argumentConfiguration, 1, strlen($argumentConfiguration) - 2);
400
401 9
        } elseif ($argumentConfiguration == '$') {
402 1
            $argumentValue = $request->getContent(false);
403
404 8
        } elseif (substr($argumentConfiguration, 0, 7) === '$files.') {
405
            /** @var FileBag $files */
406 2
            $files = $request->files;
407
408 2
            [, $filesKey, $fileArgument] = explode(".", $argumentConfiguration);
0 ignored issues
show
Bug introduced by
The variable $filesKey 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 $fileArgument 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...
409
410
            /** @var UploadedFile $file */
411 2
            $file = $files->get($filesKey);
412
413 2
            Assert::isInstanceOf($file, UploadedFile::class, sprintf(
414 2
                "Missing request-argument '%s' as uploaded file!",
415 2
                $filesKey
416
            ));
417
418
            $argumentValue = [
419 1
                'object' => $file,
420 1
                'originalname' => $file->getClientOriginalName(),
421 1
                'filename' => $file->getFilename(),
422 1
                'content' => file_get_contents($file->getPathname()),
423 1
                'mimetype' => $file->getMimeType(),
424 1
            ][$fileArgument];
425
426 6
        } elseif ($argumentConfiguration[0] == '$') {
427 3
            $argumentValue = $request->get(substr($argumentConfiguration, 1));
428
429 3
            if (is_string($argumentValue)) {
430 3
                $argumentValue = trim($argumentValue);
431
            }
432
433 4
        } elseif ($argumentConfiguration[0] == '@') {
434 1
            $argumentValue = $this->container->get(substr($argumentConfiguration, 1));
435
436 3
        } elseif ($argumentConfiguration[0] == '%') {
437
            /** @var string $key */
438 3
            $key = substr($argumentConfiguration, 1);
439
440 3
            if (is_int(strpos($key, '.'))) {
441 1
                [$key, $methodName] = explode('.', $key);
0 ignored issues
show
Bug introduced by
The variable $methodName 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...
442
443 1
                Assert::keyExists($additionalData, $key, sprintf(
444 1
                    'Missing additional-data key "%s"',
445 1
                    $key
446
                ));
447
448 1
                $argumentValue = $additionalData[$key];
449
450 1
                Assert::methodExists($argumentValue, $methodName, sprintf(
451 1
                    "Missing method '%s' on '%s'!",
452 1
                    $methodName,
453 1
                    $argumentConfiguration
454
                ));
455
456
                $argumentValue = call_user_func([$argumentValue, $methodName]);
457
458
            } else {
459 2
                Assert::keyExists($additionalData, $key, sprintf(
460 2
                    'Missing additional-data key "%s"',
461 2
                    $key
462
                ));
463
464 1
                $argumentValue = $additionalData[$key];
465
            }
466
467
        } elseif (is_int(strpos($argumentConfiguration, '#'))) {
468
            # Create as entity
469
            [$entityClass, $idRaw] = explode('#', $argumentConfiguration);
470
471
            $id = $this->resolveStringArgumentConfiguration($idRaw, $request);
472
473
            $argumentValue = $this->entityManager->find($entityClass, $id);
474
475
        } else {
476
            $argumentValue = $argumentConfiguration;
477
        }
478
479 7
        return $argumentValue;
480
    }
481
482
    /**
483
     * @param object $object
484
     *
485
     * @return mixed
486
     */
487 1
    private function callOnObject(
488
        $object,
489
        string $method,
490
        array $callArguments,
491
        Request $request,
492
        string $methodNotExistMessage
493
    ) {
494 1
        Assert::methodExists($object, $method, $methodNotExistMessage);
495
496 1
        $objectReflection = new ReflectionClass($object);
497
498
        /** @var ReflectionMethod $methodReflection */
499 1
        $methodReflection = $objectReflection->getMethod($method);
500
501 1
        $callArguments = $this->buildCallArguments(
502 1
            $methodReflection,
503 1
            [], # TODO
504 1
            $request,
505 1
            $callArguments
506
        );
507
508 1
        return call_user_func_array([$object, $method], $callArguments);
509
    }
510
511
}
512