Passed
Branch main (a99a01)
by Michael
03:11
created

BoundMethod::addDependencyForCallParameter()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
c 1
b 0
f 0
nc 6
nop 4
dl 0
loc 21
rs 8.8333
1
<?php
2
3
namespace Illuminate\Container;
4
5
use Closure;
6
use Illuminate\Contracts\Container\BindingResolutionException;
7
use InvalidArgumentException;
8
use ReflectionFunction;
9
use ReflectionMethod;
10
11
class BoundMethod
12
{
13
    /**
14
     * Call the given Closure / class@method and inject its dependencies.
15
     *
16
     * @param  \Illuminate\Container\Container  $container
17
     * @param  callable|string  $callback
18
     * @param  array  $parameters
19
     * @param  string|null  $defaultMethod
20
     * @return mixed
21
     *
22
     * @throws \ReflectionException
23
     * @throws \InvalidArgumentException
24
     */
25
    public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
26
    {
27
        if (is_string($callback) && ! $defaultMethod && method_exists($callback, '__invoke')) {
28
            $defaultMethod = '__invoke';
29
        }
30
31
        if (static::isCallableWithAtSign($callback) || $defaultMethod) {
32
            return static::callClass($container, $callback, $parameters, $defaultMethod);
33
        }
34
35
        return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
36
            return $callback(...array_values(static::getMethodDependencies($container, $callback, $parameters)));
37
        });
38
    }
39
40
    /**
41
     * Call a string reference to a class using Class@method syntax.
42
     *
43
     * @param  \Illuminate\Container\Container  $container
44
     * @param  string  $target
45
     * @param  array  $parameters
46
     * @param  string|null  $defaultMethod
47
     * @return mixed
48
     *
49
     * @throws \InvalidArgumentException
50
     */
51
    protected static function callClass($container, $target, array $parameters = [], $defaultMethod = null)
52
    {
53
        $segments = explode('@', $target);
54
55
        // We will assume an @ sign is used to delimit the class name from the method
56
        // name. We will split on this @ sign and then build a callable array that
57
        // we can pass right back into the "call" method for dependency binding.
58
        $method = count($segments) === 2
59
            ? $segments[1] : $defaultMethod;
60
61
        if (is_null($method)) {
62
            throw new InvalidArgumentException('Method not provided.');
63
        }
64
65
        return static::call(
66
            $container, [$container->make($segments[0]), $method], $parameters
67
        );
68
    }
69
70
    /**
71
     * Call a method that has been bound to the container.
72
     *
73
     * @param  \Illuminate\Container\Container  $container
74
     * @param  callable  $callback
75
     * @param  mixed  $default
76
     * @return mixed
77
     *
78
     * @throws \ReflectionException
79
     */
80
    protected static function callBoundMethod($container, $callback, $default)
81
    {
82
        if (! is_array($callback)) {
83
            return Util::unwrapIfClosure($default);
84
        }
85
86
        // Here we need to turn the array callable into a Class@method string we can use to
87
        // examine the container and see if there are any method bindings for this given
88
        // method. If there are, we can call this method binding callback immediately.
89
        $method = static::normalizeMethod($callback);
90
91
        if ($container->hasMethodBinding($method)) {
92
            $reflectDefault = new ReflectionFunction($default);
93
94
            return $container->callMethodBinding(
95
                $method,
96
                $callback[0],
97
                $reflectDefault->getStaticVariables()['parameters']
98
            );
99
        }
100
101
        return Util::unwrapIfClosure($default);
102
    }
103
104
    /**
105
     * Normalize the given callback into a Class@method string.
106
     *
107
     * @param  callable  $callback
108
     * @return string
109
     */
110
    protected static function normalizeMethod($callback)
111
    {
112
        $class = is_string($callback[0]) ? $callback[0] : get_class($callback[0]);
113
114
        return "{$class}@{$callback[1]}";
115
    }
116
117
    /**
118
     * Get all dependencies for a given method.
119
     *
120
     * @param  \Illuminate\Container\Container  $container
121
     * @param  callable|string  $callback
122
     * @param  array  $parameters
123
     * @return array
124
     *
125
     * @throws \ReflectionException
126
     */
127
    protected static function getMethodDependencies($container, $callback, array $parameters = [])
128
    {
129
        $dependencies = [];
130
131
        foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
132
            static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
133
        }
134
135
        return array_merge($dependencies, array_values($parameters));
136
    }
137
138
    /**
139
     * Get the proper reflection instance for the given callback.
140
     *
141
     * @param  callable|string  $callback
142
     * @return \ReflectionFunctionAbstract
143
     *
144
     * @throws \ReflectionException
145
     */
146
    protected static function getCallReflector($callback)
147
    {
148
        if (is_string($callback) && strpos($callback, '::') !== false) {
149
            $callback = explode('::', $callback);
150
        } elseif (is_object($callback) && ! $callback instanceof Closure) {
151
            $callback = [$callback, '__invoke'];
152
        }
153
154
        return is_array($callback)
155
            ? new ReflectionMethod($callback[0], $callback[1])
156
            : new ReflectionFunction($callback);
157
    }
158
159
    /**
160
     * Get the dependency for the given call parameter.
161
     *
162
     * @param  \Illuminate\Container\Container  $container
163
     * @param  \ReflectionParameter  $parameter
164
     * @param  array  $parameters
165
     * @param  array  $dependencies
166
     * @return void
167
     *
168
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
169
     */
170
    protected static function addDependencyForCallParameter($container, $parameter,
171
                                                            array &$parameters, &$dependencies)
172
    {
173
        if (array_key_exists($paramName = $parameter->getName(), $parameters)) {
174
            $dependencies[] = $parameters[$paramName];
175
176
            unset($parameters[$paramName]);
177
        } elseif (! is_null($className = Util::getParameterClassName($parameter))) {
178
            if (array_key_exists($className, $parameters)) {
179
                $dependencies[] = $parameters[$className];
180
181
                unset($parameters[$className]);
182
            } else {
183
                $dependencies[] = $container->make($className);
184
            }
185
        } elseif ($parameter->isDefaultValueAvailable()) {
186
            $dependencies[] = $parameter->getDefaultValue();
187
        } elseif (! $parameter->isOptional() && ! array_key_exists($paramName, $parameters)) {
188
            $message = "Unable to resolve dependency [{$parameter}] in class {$parameter->getDeclaringClass()->getName()}";
189
190
            throw new BindingResolutionException($message);
191
        }
192
    }
193
194
    /**
195
     * Determine if the given string is in Class@method syntax.
196
     *
197
     * @param  mixed  $callback
198
     * @return bool
199
     */
200
    protected static function isCallableWithAtSign($callback)
201
    {
202
        return is_string($callback) && strpos($callback, '@') !== false;
203
    }
204
}
205