Completed
Push — master ( b03604...27d676 )
by Gerrit
03:42
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 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
use Symfony\Component\HttpFoundation\FileBag;
30
use Symfony\Component\HttpFoundation\File\UploadedFile;
31
32
final class ArgumentCompiler implements ArgumentCompilerInterface
33
{
34
35
    /**
36
     * @var ContainerInterface
37
     */
38
    private $container;
39
40
    /**
41
     * @var EntityManagerInterface
42
     */
43
    private $entityManager;
44
45 12
    public function __construct(
46
        ContainerInterface $container,
47
        EntityManagerInterface $entityManager
48
    ) {
49 12
        $this->container = $container;
50 12
        $this->entityManager = $entityManager;
51 12
    }
52
53 9
    public function buildArguments(
54
        array $argumentsConfiguration,
55
        Request $request,
56
        array $additionalData = array()
57
    ): array {
58
        /** @var array<int, mixed> $routeArguments */
59 9
        $routeArguments = array();
60
61 9
        foreach ($argumentsConfiguration as $key => $argumentConfiguration) {
62
            /** @var array|string $argumentConfiguration */
63
64
            /** @var string|null $parameterTypeName */
65 9
            $parameterTypeName = null;
66
67 9
            if (isset($argumentConfiguration['entity-class'])) {
68
                $parameterTypeName = $argumentConfiguration['entity-class'];
69
            }
70
71
            /** @var mixed $argumentValue */
72 9
            $argumentValue = $this->resolveArgumentConfiguration(
73 9
                $argumentConfiguration,
74 9
                $request,
75 9
                $parameterTypeName,
76 9
                $additionalData
77
            );
78
79 6
            $routeArguments[$key] = $argumentValue;
80
        }
81
82 5
        return $routeArguments;
83
    }
84
85 4
    public function buildCallArguments(
86
        ReflectionFunctionAbstract $routineReflection,
87
        array $argumentsConfiguration,
88
        Request $request,
89
        array $predefinedArguments = array(),
90
        array $additionalData = array()
91
    ): array {
92
        /** @var array<int, mixed> $callArguments */
93 4
        $callArguments = array();
94
95 4
        foreach ($routineReflection->getParameters() as $index => $parameterReflection) {
96
            /** @var ReflectionParameter $parameterReflection */
97
98 3
            if (isset($predefinedArguments[$index])) {
99
                $callArguments[] = $predefinedArguments[$index];
100
                continue;
101
            }
102
103
            /** @var string $parameterName */
104 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...
105
106
            /** @var mixed $requestValue */
107 3
            $requestValue = $request->get($parameterName);
108
109
            /** @var string|null $parameterTypeName */
110 3
            $parameterTypeName = null;
111
112 3
            if ($parameterReflection->hasType()) {
113
                /** @var ReflectionType|null $parameterType */
114 2
                $parameterType = $parameterReflection->getType();
115
116 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...
117 2
                    $parameterTypeName = $parameterType->__toString();
118
                }
119
            }
120
121 3
            if (isset($argumentsConfiguration[$parameterName])) {
122
                /** @var array|string $argumentConfiguration */
123 3
                $argumentConfiguration = $argumentsConfiguration[$parameterName];
124
125
                /** @var mixed $argumentValue */
126 3
                $argumentValue = $argumentConfiguration;
127
128 3
                if (is_string($argumentConfiguration) || is_array($argumentConfiguration)) {
129 3
                    $argumentValue = $this->resolveArgumentConfiguration(
130 3
                        $argumentConfiguration,
131 3
                        $request,
132 3
                        $parameterTypeName,
133 3
                        $additionalData
134
                    );
135
                }
136
137 1
                $callArguments[$index] = $argumentValue;
138
139 2
            } elseif (isset($argumentsConfiguration[$index])) {
140
                /** @var array|string $argumentConfiguration */
141
                $argumentConfiguration = $argumentsConfiguration[$index];
142
143
                /** @var mixed $argumentValue */
144
                $argumentValue = $argumentConfiguration;
145
146
                if (is_string($argumentConfiguration) || is_array($argumentConfiguration)) {
147
                    $argumentValue = $this->resolveArgumentConfiguration(
148
                        $argumentConfiguration,
149
                        $request,
150
                        $parameterTypeName,
151
                        $additionalData
152
                    );
153
                }
154
155
                $callArguments[$index] = $argumentValue;
156
157 2
            } elseif (!is_null($requestValue)) {
158 1
                if (!is_null($parameterTypeName)) {
159
                    /** @psalm-suppress UndefinedClass ValueObjects\ValueObjectInterface does not exist */
160
                    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...
161
                        $argumentValue = $parameterTypeName::fromNative($requestValue);
162
163
                        $callArguments[$index] = $argumentValue;
164
                    }
165
166
                } else {
167 1
                    $callArguments[$index] = $requestValue;
168
                }
169
170 1
            } elseif ($parameterTypeName === Request::class) {
171
                $callArguments[$index] = $request;
172
173
            } else {
174
                try {
175 1
                    $callArguments[$index] = $parameterReflection->getDefaultValue();
176
177 1
                } catch (ReflectionException $exception) {
178 1
                    throw new InvalidArgumentException(sprintf(
179 1
                        "Missing argument '%s' for the call to '%s'!",
180 1
                        $parameterName,
181 2
                        $routineReflection->getName()
182
                    ));
183
                }
184
            }
185
        }
186
187 2
        return $callArguments;
188
    }
189
190
    /**
191
     * @param array|string $argumentConfiguration
192
     *
193
     * @return mixed
194
     */
195 12
    public function resolveArgumentConfiguration(
196
        $argumentConfiguration,
197
        Request $request,
198
        ?string $parameterTypeName,
199
        array $additionalData = array()
200
    ) {
201
        /** @var mixed $argumentValue */
202 12
        $argumentValue = null;
203
204 12
        if (is_array($argumentConfiguration)) {
205 3
            if (isset($argumentConfiguration['entity-class'])) {
206
                $parameterTypeName = $argumentConfiguration['entity-class'];
207
            }
208
209 3
            if (isset($argumentConfiguration['service-id'])) {
210 3
                $argumentValue = $this->container->get($argumentConfiguration['service-id']);
211
212 3
                Assert::object($argumentValue, sprintf(
213 3
                    "Did not find service '%s'!",
214 3
                    $argumentConfiguration['service-id']
215
                ));
216
217
            } elseif (isset($argumentConfiguration['entity-id'])) {
218
                $argumentValue = $this->resolveStringArgumentConfiguration(
219
                    $argumentConfiguration['entity-id'],
220
                    $request
221
                );
222
223
            } elseif (class_exists($parameterTypeName)) {
224
                $argumentValue = $this->resolveStringArgumentConfiguration(
225
                    $parameterTypeName,
226
                    $request
227
                );
228
            }
229
230 2
            if (isset($argumentConfiguration['method'])) {
231 2
                $methodReflection = new ReflectionMethod($argumentValue, $argumentConfiguration['method']);
232
233 2
                if (!isset($argumentConfiguration['arguments'])) {
234 1
                    $argumentConfiguration['arguments'] = [];
235
                }
236
237
                /** @var array $callArguments */
238 2
                $callArguments = $this->buildCallArguments(
239 2
                    $methodReflection,
240 2
                    $argumentConfiguration['arguments'],
241 2
                    $request
242
                );
243
244 1
                $argumentValue = $methodReflection->invokeArgs($argumentValue, $callArguments);
245
            }
246
247
        } else {
248 10
            $argumentValue = $this->resolveStringArgumentConfiguration(
249 10
                $argumentConfiguration,
250 10
                $request,
251 10
                $additionalData
252
            );
253
        }
254
255 7
        if (!empty($parameterTypeName) && !is_object($argumentValue)) {
256 1
            if (class_exists($parameterTypeName)) {
257
                /** @psalm-suppress UndefinedClass ValueObjects\ValueObjectInterface does not exist */
258 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...
259
                    $argumentValue = call_user_func("{$parameterTypeName}::fromNative", $argumentValue);
260
261
                } else {
262 1
                    $argumentValue = $this->entityManager->find($parameterTypeName, $argumentValue);
263
                }
264
            }
265
        }
266
267 7
        return $argumentValue;
268
    }
269
270
    /**
271
     * @return mixed
272
     */
273 10
    private function resolveStringArgumentConfiguration(
274
        string $argumentConfiguration,
275
        Request $request,
276
        array $additionalData = array()
277
    ) {
278
        /** @var mixed $argumentValue */
279 10
        $argumentValue = null;
280
281 10
        $argumentConfiguration = trim($argumentConfiguration);
282
283 10
        if (is_int(strpos($argumentConfiguration, '::'))) {
284 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...
285
286
            /** @var array<string> $callArgumentConfigurations */
287 2
            $callArgumentConfigurations = array();
288
289 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...
290
                $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...
291
                [$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...
292
293
                foreach (explode(",", $rawArguments) as $rawArgument) {
294
                    /** @var string $rawArgument */
295
296
                    $callArgumentConfigurations[] = trim($rawArgument);
297
                }
298
            }
299
300 2
            if (!empty($factoryClass)) {
301 2
                if ($factoryClass[0] == '@') {
302
                    /** @var string $factoryServiceId */
303 2
                    $factoryServiceId = substr($factoryClass, 1);
304
305
                    /** @var object $factoryObject */
306 2
                    $factoryObject = $this->container->get($factoryServiceId);
307
308 2
                    Assert::object($factoryObject, sprintf(
309 2
                        "Could not find service with id '%s'!",
310 2
                        $factoryServiceId
311
                    ));
312
313 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...
314 2
                        "Method '%s' does not exist on service '%s'! (Class '%s')",
315 2
                        $factoryMethod,
316 2
                        $factoryServiceId,
317 2
                        get_class($factoryObject)
318
                    ));
319
320
                    /** @var array<int, mixed> $callArguments */
321 1
                    $callArguments = $this->buildCallArguments(
322 1
                        new ReflectionMethod($factoryObject, $factoryMethod),
323 1
                        $callArgumentConfigurations,
324 1
                        $request
325
                    );
326
327 1
                    $argumentValue = $this->callOnObject(
328 1
                        $factoryObject,
329 1
                        $factoryMethod,
330 1
                        $callArguments,
331 1
                        $request,
332 1
                        sprintf(
333 1
                            "Did not find service with id '%s' that has a method '%s'!",
334 1
                            $factoryServiceId,
335 1
                            $factoryMethod
336
                        )
337
                    );
338
339
                } elseif (is_int(strpos($factoryClass, '#'))) {
340
                    # Create by call on entity
341
                    [$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...
342
343
                    $id = $this->resolveStringArgumentConfiguration($idRaw, $request, $additionalData);
344
345
                    /** @var object $entity */
346
                    $entity = $this->entityManager->find($entityClass, $id);
347
348
                    Assert::object($entity, sprintf(
349
                        "Could not find entity '%s' with id '%s'!",
350
                        $entityClass,
351
                        $id
352
                    ));
353
354
                    /** @var array<int, mixed> $callArguments */
355
                    $callArguments = $this->buildCallArguments(
356
                        new ReflectionMethod($entity, $factoryMethod),
357
                        $callArgumentConfigurations,
358
                        $request
359
                    );
360
361
                    $argumentValue = $this->callOnObject(
362
                        $entity,
363
                        $factoryMethod,
364
                        $callArguments,
365
                        $request,
366
                        sprintf(
367
                            "Entity '%s' does not have method '%s'!",
368
                            $entityClass,
369
                            $factoryMethod
370
                        )
371
                    );
372
373
                } else {
374
                    $callArguments = array();
375
376
                    if (is_int(strpos($argumentConfiguration, '('))) {
377
                        $argumentConfiguration = str_replace(")", "", $argumentConfiguration);
378
                        [$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...
379
380
                        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...
381
                            foreach (explode(',', $callArgumentsRaw) as $callArgumentRaw) {
382
                                $callArguments[] = $this->resolveStringArgumentConfiguration(
383
                                    $callArgumentRaw,
384
                                    $request,
385
                                    $additionalData
386
                                );
387
                            }
388
                        }
389
                    }
390
391
                    # Create by static factory-method of other class
392 1
                    $argumentValue = call_user_func_array($argumentConfiguration, $callArguments);
393
                }
394
395 1
            } else {
396
                # TODO: What to do here? What could "::Something" be? A template?
397
            }
398
399 10
        } elseif ($argumentConfiguration[0] == "'" && $argumentConfiguration[strlen($argumentConfiguration) - 1] == "'") {
400 1
            $argumentValue = substr($argumentConfiguration, 1, strlen($argumentConfiguration) - 2);
401
402 9
        } elseif ($argumentConfiguration == '$') {
403 1
            $argumentValue = $request->getContent(false);
404
405 8
        } elseif (substr($argumentConfiguration, 0, 7) === '$files.') {
406
            /** @var FileBag $files */
407 2
            $files = $request->files;
408
409 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...
410
411
            /** @var UploadedFile $file */
412 2
            $file = $files->get($filesKey);
413
414 2
            Assert::isInstanceOf($file, UploadedFile::class, sprintf(
415 2
                "Missing request-argument '%s' as uploaded file!",
416 2
                $filesKey
417
            ));
418
419
            $argumentValue = [
420 1
                'object' => $file,
421 1
                'originalname' => $file->getClientOriginalName(),
422 1
                'filename' => $file->getFilename(),
423 1
                'content' => file_get_contents($file->getPathname()),
424 1
                'mimetype' => $file->getMimeType(),
425 1
            ][$fileArgument];
426
427 6
        } elseif ($argumentConfiguration[0] == '$') {
428 3
            $argumentValue = $request->get(substr($argumentConfiguration, 1));
429
430 4
        } elseif ($argumentConfiguration[0] == '@') {
431 1
            $argumentValue = $this->container->get(substr($argumentConfiguration, 1));
432
433 3
        } elseif ($argumentConfiguration[0] == '%') {
434
            /** @var string $key */
435 3
            $key = substr($argumentConfiguration, 1);
436
437 3
            if (is_int(strpos($key, '.'))) {
438 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...
439
440 1
                Assert::keyExists($additionalData, $key, sprintf(
441 1
                    'Missing additional-data key "%s"',
442 1
                    $key
443
                ));
444
445 1
                $argumentValue = $additionalData[$key];
446
447 1
                Assert::methodExists($argumentValue, $methodName, sprintf(
448 1
                    "Missing method '%s' on '%s'!",
449 1
                    $methodName,
450 1
                    $argumentConfiguration
451
                ));
452
453
                $argumentValue = call_user_func([$argumentValue, $methodName]);
454
455
            } else {
456 2
                Assert::keyExists($additionalData, $key, sprintf(
457 2
                    'Missing additional-data key "%s"',
458 2
                    $key
459
                ));
460
461 1
                $argumentValue = $additionalData[$key];
462
            }
463
464
        } elseif (is_int(strpos($argumentConfiguration, '#'))) {
465
            # Create as entity
466
            [$entityClass, $idRaw] = explode('#', $argumentConfiguration);
467
468
            $id = $this->resolveStringArgumentConfiguration($idRaw, $request);
469
470
            $argumentValue = $this->entityManager->find($entityClass, $id);
471
472
        } else {
473
            $argumentValue = $argumentConfiguration;
474
        }
475
476 7
        return $argumentValue;
477
    }
478
479
    /**
480
     * @param object $object
481
     *
482
     * @return mixed
483
     */
484 1
    private function callOnObject(
485
        $object,
486
        string $method,
487
        array $callArguments,
488
        Request $request,
489
        string $methodNotExistMessage
490
    ) {
491 1
        Assert::methodExists($object, $method, $methodNotExistMessage);
492
493 1
        $objectReflection = new ReflectionClass($object);
494
495
        /** @var ReflectionMethod $methodReflection */
496 1
        $methodReflection = $objectReflection->getMethod($method);
497
498 1
        $callArguments = $this->buildCallArguments(
499 1
            $methodReflection,
500 1
            [], # TODO
501 1
            $request,
502 1
            $callArguments
503
        );
504
505 1
        return call_user_func_array([$object, $method], $callArguments);
506
    }
507
508
}
509