1<?php
2
3declare(strict_types=1);
4
5namespace DI\Definition\Resolver;
6
7use DI\Definition\Definition;
8use DI\Definition\Exception\InvalidDefinition;
9use DI\Definition\ObjectDefinition\MethodInjection;
10use ReflectionMethod;
11use ReflectionParameter;
12
13/**
14 * Resolves parameters for a function call.
15 *
16 * @since  4.2
17 * @author Matthieu Napoli <matthieu@mnapoli.fr>
18 */
19class ParameterResolver
20{
21    /**
22     * @var DefinitionResolver
23     */
24    private $definitionResolver;
25
26    /**
27     * @param DefinitionResolver $definitionResolver Will be used to resolve nested definitions.
28     */
29    public function __construct(DefinitionResolver $definitionResolver)
30    {
31        $this->definitionResolver = $definitionResolver;
32    }
33
34    /**
35     * @throws InvalidDefinition A parameter has no value defined or guessable.
36     * @return array Parameters to use to call the function.
37     */
38    public function resolveParameters(
39        MethodInjection $definition = null,
40        ReflectionMethod $method = null,
41        array $parameters = []
42    ) {
43        $args = [];
44
45        if (! $method) {
46            return $args;
47        }
48
49        $definitionParameters = $definition ? $definition->getParameters() : [];
50
51        foreach ($method->getParameters() as $index => $parameter) {
52            if (array_key_exists($parameter->getName(), $parameters)) {
53                // Look in the $parameters array
54                $value = &$parameters[$parameter->getName()];
55            } elseif (array_key_exists($index, $definitionParameters)) {
56                // Look in the definition
57                $value = &$definitionParameters[$index];
58            } else {
59                // If the parameter is optional and wasn't specified, we take its default value
60                if ($parameter->isDefaultValueAvailable() || $parameter->isOptional()) {
61                    $args[] = $this->getParameterDefaultValue($parameter, $method);
62                    continue;
63                }
64
65                throw new InvalidDefinition(sprintf(
66                    'Parameter $%s of %s has no value defined or guessable',
67                    $parameter->getName(),
68                    $this->getFunctionName($method)
69                ));
70            }
71
72            // Nested definitions
73            if ($value instanceof Definition) {
74                // If the container cannot produce the entry, we can use the default parameter value
75                if ($parameter->isOptional() && ! $this->definitionResolver->isResolvable($value)) {
76                    $value = $this->getParameterDefaultValue($parameter, $method);
77                } else {
78                    $value = $this->definitionResolver->resolve($value);
79                }
80            }
81
82            $args[] = &$value;
83        }
84
85        return $args;
86    }
87
88    /**
89     * Returns the default value of a function parameter.
90     *
91     * @throws InvalidDefinition Can't get default values from PHP internal classes and functions
92     * @return mixed
93     */
94    private function getParameterDefaultValue(ReflectionParameter $parameter, ReflectionMethod $function)
95    {
96        try {
97            return $parameter->getDefaultValue();
98        } catch (\ReflectionException $e) {
99            throw new InvalidDefinition(sprintf(
100                'The parameter "%s" of %s has no type defined or guessable. It has a default value, '
101                . 'but the default value can\'t be read through Reflection because it is a PHP internal class.',
102                $parameter->getName(),
103                $this->getFunctionName($function)
104            ));
105        }
106    }
107
108    private function getFunctionName(ReflectionMethod $method) : string
109    {
110        return $method->getName() . '()';
111    }
112}
113