BoundMethod.php 7.4 KB

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