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

ArgumentCompiler::callOnObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 10
cts 10
cp 1
rs 9.552
c 0
b 0
f 0
cc 1
nc 1
nop 5
crap 1
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 11
    public function __construct(
46
        ContainerInterface $container,
47
        EntityManagerInterface $entityManager
48
    ) {
49 11
        $this->container = $container;
50 11
        $this->entityManager = $entityManager;
51 11
    }
52
53 8
    public function buildArguments(
54
        array $argumentsConfiguration,
55
        Request $request,
56
        array $additionalData = array()
57
    ): array {
58
        /** @var array<int, mixed> $routeArguments */
59 8
        $routeArguments = array();
60
61 8
        foreach ($argumentsConfiguration as $key => $argumentConfiguration) {
62
            /** @var array|string $argumentConfiguration */
63
64
            /** @var string|null $parameterTypeName */
65 8
            $parameterTypeName = null;
66
67 8
            if (isset($argumentConfiguration['entity-class'])) {
68
                $parameterTypeName = $argumentConfiguration['entity-class'];
69
            }
70
71
            /** @var mixed $argumentValue */
72 8
            $argumentValue = $this->resolveArgumentConfiguration(
73 8
                $argumentConfiguration,
74 8
                $request,
75 8
                $parameterTypeName,
76 8
                $additionalData
77
            );
78
79 5
            $routeArguments[$key] = $argumentValue;
80
        }
81
82 4
        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 11
    public function resolveArgumentConfiguration(
196
        $argumentConfiguration,
197
        Request $request,
198
        ?string $parameterTypeName,
199
        array $additionalData = array()
200
    ) {
201
        /** @var mixed $argumentValue */
202 11
        $argumentValue = null;
203
204 11
        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 9
            $argumentValue = $this->resolveStringArgumentConfiguration(
249 9
                $argumentConfiguration,
250 9
                $request,
251 9
                $additionalData
252
            );
253
        }
254
255 6
        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 6
        return $argumentValue;
268
    }
269
270
    /**
271
     * @return mixed
272
     */
273 9
    private function resolveStringArgumentConfiguration(
274
        string $argumentConfiguration,
275
        Request $request,
276
        array $additionalData = array()
277
    ) {
278
        /** @var mixed $argumentValue */
279 9
        $argumentValue = null;
280
281 9
        if (is_int(strpos($argumentConfiguration, '::'))) {
282 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...
283
284
            /** @var array<string> $callArgumentConfigurations */
285 2
            $callArgumentConfigurations = array();
286
287 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...
288
                $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...
289
                [$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...
290
291
                foreach (explode(",", $rawArguments) as $rawArgument) {
292
                    /** @var string $rawArgument */
293
294
                    $callArgumentConfigurations[] = trim($rawArgument);
295
                }
296
            }
297
298 2
            if (!empty($factoryClass)) {
299 2
                if ($factoryClass[0] == '@') {
300
                    /** @var string $factoryServiceId */
301 2
                    $factoryServiceId = substr($factoryClass, 1);
302
303
                    /** @var object $factoryObject */
304 2
                    $factoryObject = $this->container->get($factoryServiceId);
305
306 2
                    Assert::object($factoryObject, sprintf(
307 2
                        "Could not find service with id '%s'!",
308 2
                        $factoryServiceId
309
                    ));
310
311 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...
312 2
                        "Method '%s' does not exist on service '%s'! (Class '%s')",
313 2
                        $factoryMethod,
314 2
                        $factoryServiceId,
315 2
                        get_class($factoryObject)
316
                    ));
317
318
                    /** @var array<int, mixed> $callArguments */
319 1
                    $callArguments = $this->buildCallArguments(
320 1
                        new ReflectionMethod($factoryObject, $factoryMethod),
321 1
                        $callArgumentConfigurations,
322 1
                        $request
323
                    );
324
325 1
                    $argumentValue = $this->callOnObject(
326 1
                        $factoryObject,
327 1
                        $factoryMethod,
328 1
                        $callArguments,
329 1
                        $request,
330 1
                        sprintf(
331 1
                            "Did not find service with id '%s' that has a method '%s'!",
332 1
                            $factoryServiceId,
333 1
                            $factoryMethod
334
                        )
335
                    );
336
337
                } elseif (is_int(strpos($factoryClass, '#'))) {
338
                    # Create by call on entity
339
                    [$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...
340
341
                    $id = $this->resolveStringArgumentConfiguration($idRaw, $request, $additionalData);
342
343
                    /** @var object $entity */
344
                    $entity = $this->entityManager->find($entityClass, $id);
345
346
                    Assert::object($entity, sprintf(
347
                        "Could not find entity '%s' with id '%s'!",
348
                        $entityClass,
349
                        $id
350
                    ));
351
352
                    /** @var array<int, mixed> $callArguments */
353
                    $callArguments = $this->buildCallArguments(
354
                        new ReflectionMethod($entity, $factoryMethod),
355
                        $callArgumentConfigurations,
356
                        $request
357
                    );
358
359
                    $argumentValue = $this->callOnObject(
360
                        $entity,
361
                        $factoryMethod,
362
                        $callArguments,
363
                        $request,
364
                        sprintf(
365
                            "Entity '%s' does not have method '%s'!",
366
                            $entityClass,
367
                            $factoryMethod
368
                        )
369
                    );
370
371
                } else {
372
                    $callArguments = array();
373
374
                    if (is_int(strpos($argumentConfiguration, '('))) {
375
                        $argumentConfiguration = str_replace(")", "", $argumentConfiguration);
376
                        [$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...
377
378
                        foreach (explode(',', $callArgumentsRaw) as $callArgumentRaw) {
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
                            $callArguments[] = $this->resolveStringArgumentConfiguration(
380
                                $callArgumentRaw,
381
                                $request,
382
                                $additionalData
383
                            );
384
                        }
385
                    }
386
387
                    # Create by static factory-method of other class
388 1
                    $argumentValue = call_user_func_array($argumentConfiguration, $callArguments);
389
                }
390
391 1
            } else {
392
                # TODO: What to do here? What could "::Something" be? A template?
393
            }
394
395 9
        } elseif ($argumentConfiguration[0] == "'" && $argumentConfiguration[strlen($argumentConfiguration) - 1] == "'") {
396 1
            $argumentValue = substr($argumentConfiguration, 1, strlen($argumentConfiguration) - 2);
397
398 8
        } elseif ($argumentConfiguration == '$') {
399 1
            $argumentValue = $request->getContent(false);
400
401 7
        } elseif (substr($argumentConfiguration, 0, 7) === '$files.') {
402
            /** @var FileBag $files */
403 1
            $files = $request->files;
404
405 1
            [, $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...
406
407
            /** @var UploadedFile $file */
408 1
            $file = $files->get($filesKey);
409
410 1
            Assert::isInstanceOf($file, UploadedFile::class, sprintf(
411 1
                "Missing request-argument '%s' as uploaded file!",
412 1
                $filesKey
413
            ));
414
415
            $argumentValue = [
416
                'object' => $file,
417
                'originalname' => $file->getClientOriginalName(),
418
                'filename' => $file->getFilename(),
419
                'content' => file_get_contents($file->getPathname()),
420
                'mimetype' => $file->getMimeType(),
421
            ][$fileArgument];
422
423 6
        } elseif ($argumentConfiguration[0] == '$') {
424 3
            $argumentValue = $request->get(substr($argumentConfiguration, 1));
425
426 4
        } elseif ($argumentConfiguration[0] == '@') {
427 1
            $argumentValue = $this->container->get(substr($argumentConfiguration, 1));
428
429 3
        } elseif ($argumentConfiguration[0] == '%') {
430
            /** @var string $key */
431 3
            $key = substr($argumentConfiguration, 1);
432
433 3
            if (is_int(strpos($key, '.'))) {
434 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...
435
436 1
                Assert::keyExists($additionalData, $key, sprintf(
437 1
                    'Missing additional-data key "%s"',
438 1
                    $key
439
                ));
440
441 1
                $argumentValue = $additionalData[$key];
442
443 1
                Assert::methodExists($argumentValue, $methodName, sprintf(
444 1
                    "Missing method '%s' on '%s'!",
445 1
                    $methodName,
446 1
                    $argumentConfiguration
447
                ));
448
449
                $argumentValue = call_user_func([$argumentValue, $methodName]);
450
451
            } else {
452 2
                Assert::keyExists($additionalData, $key, sprintf(
453 2
                    'Missing additional-data key "%s"',
454 2
                    $key
455
                ));
456
457 1
                $argumentValue = $additionalData[$key];
458
            }
459
460
        } elseif (is_int(strpos($argumentConfiguration, '#'))) {
461
            # Create as entity
462
            [$entityClass, $idRaw] = explode('#', $argumentConfiguration);
463
464
            $id = $this->resolveStringArgumentConfiguration($idRaw, $request);
465
466
            $argumentValue = $this->entityManager->find($entityClass, $id);
467
468
        } else {
469
            $argumentValue = $argumentConfiguration;
470
        }
471
472 6
        return $argumentValue;
473
    }
474
475
    /**
476
     * @param object $object
477
     *
478
     * @return mixed
479
     */
480 1
    private function callOnObject(
481
        $object,
482
        string $method,
483
        array $callArguments,
484
        Request $request,
485
        string $methodNotExistMessage
486
    ) {
487 1
        Assert::methodExists($object, $method, $methodNotExistMessage);
488
489 1
        $objectReflection = new ReflectionClass($object);
490
491
        /** @var ReflectionMethod $methodReflection */
492 1
        $methodReflection = $objectReflection->getMethod($method);
493
494 1
        $callArguments = $this->buildCallArguments(
495 1
            $methodReflection,
496 1
            [], # TODO
497 1
            $request,
498 1
            $callArguments
499
        );
500
501 1
        return call_user_func_array([$object, $method], $callArguments);
502
    }
503
504
}
505