Skip to content

[5.8] Fix for container app()->call() method #26920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 95 additions & 44 deletions src/Illuminate/Container/BoundMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,80 @@
use Closure;
use ReflectionMethod;
use ReflectionFunction;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Illuminate\Contracts\Container\Container as ContainerContract;

class BoundMethod
{
/**
* @var ContainerContract
*/
private static $container;

/**
* Call the given Closure / class@method and inject its dependencies.
*
* @param \Illuminate\Container\Container $container
* @param ContainerContract $container
* @param callable|string $callback
* @param array $parameters
* @param array $inputData
* @param string|null $defaultMethod
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
public static function call(ContainerContract $container, $callback, array $inputData = [], $defaultMethod = null)
{
self::setContainer($container);

if (static::isCallableWithAtSign($callback) || $defaultMethod) {
return static::callClass($container, $callback, $parameters, $defaultMethod);
return static::callClass($callback, $inputData, $defaultMethod);
}

return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return static::callBoundMethod($callback, function () use ($callback, $inputData) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
$callback, static::getMethodDependencies($callback, $inputData)
);
});
}

/**
* Call a string reference to a class using Class@method syntax.
*
* @param \Illuminate\Container\Container $container
* @param string $target
* @param array $parameters
* @param string|null $defaultMethod
* @return mixed
*
* @throws \InvalidArgumentException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected static function callClass($container, $target, array $parameters = [], $defaultMethod = null)
protected static function callClass($target, array $parameters = [], $defaultMethod = null)
{
$segments = explode('@', $target);

// We will assume an @ sign is used to delimit the class name from the method
// name. We will split on this @ sign and then build a callable array that
// we can pass right back into the "call" method for dependency binding.
$method = count($segments) === 2
? $segments[1] : $defaultMethod;
$method = count($segments) === 2 ? $segments[1] : $defaultMethod;

if (is_null($method)) {
throw new InvalidArgumentException('Method not provided.');
}

return static::call(
$container, [$container->make($segments[0]), $method], $parameters
self::$container, [self::$container->make($segments[0]), $method], $parameters
);
}

/**
* Call a method that has been bound to the container.
*
* @param \Illuminate\Container\Container $container
* @param callable $callback
* @param mixed $default
* @return mixed
*/
protected static function callBoundMethod($container, $callback, $default)
protected static function callBoundMethod($callback, $default)
{
if (! is_array($callback)) {
return $default instanceof Closure ? $default() : $default;
Expand All @@ -80,8 +89,8 @@ protected static function callBoundMethod($container, $callback, $default)
// method. If there are, we can call this method binding callback immediately.
$method = static::normalizeMethod($callback);

if ($container->hasMethodBinding($method)) {
return $container->callMethodBinding($method, $callback[0]);
if (self::$container->hasMethodBinding($method)) {
return self::$container->callMethodBinding($method, $callback[0]);
}

return $default instanceof Closure ? $default() : $default;
Expand All @@ -103,26 +112,37 @@ protected static function normalizeMethod($callback)
/**
* Get all dependencies for a given method.
*
* @param \Illuminate\Container\Container $container
* @param callable|string $callback
* @param array $parameters
* @param array $inputData
* @return array
*
* @throws \ReflectionException
*/
protected static function getMethodDependencies($container, $callback, array $parameters = [])
protected static function getMethodDependencies($callback, array $inputData = [])
{
$dependencies = [];
$signature = static::getCallReflector($callback)->getParameters();

// In case the method has no explicit input parameters we will
// call that with whatever input data available to us since
// they may have used func_get_args() to catch the input.
if (count($signature) === 0) {
return $inputData;
}

foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
// When we receive the input as an indexed array and the count of passed arguments
// is not less than the declared parameters, it means that we are provided with
// everything needed, So the IOC container should not bother about injection.
if (! Arr::isAssoc($inputData) && (count($signature) <= count($inputData))) {
return $inputData;
}

return array_merge($dependencies, $parameters);
return static::addDependenciesToInputData($signature, $inputData);
}

/**
* Get the proper reflection instance for the given callback.
*
* @param callable|string $callback
* @param callable|string $callback
* @return \ReflectionFunctionAbstract
*
* @throws \ReflectionException
Expand All @@ -139,40 +159,71 @@ protected static function getCallReflector($callback)
}

/**
* Get the dependency for the given call parameter.
* Add the dependencies to the input data.
*
* @param \Illuminate\Container\Container $container
* @param \ReflectionParameter $parameter
* @param array $parameters
* @param array $dependencies
* @return void
* @param array $signature
* @param array $inputData
* @return array
*/
protected static function addDependencyForCallParameter($container, $parameter,
array &$parameters, &$dependencies)
protected static function addDependenciesToInputData(array $signature, array $inputData)
{
if (array_key_exists($parameter->name, $parameters)) {
$dependencies[] = $parameters[$parameter->name];

unset($parameters[$parameter->name]);
} elseif ($parameter->getClass() && array_key_exists($parameter->getClass()->name, $parameters)) {
$dependencies[] = $parameters[$parameter->getClass()->name];

unset($parameters[$parameter->getClass()->name]);
} elseif ($parameter->getClass()) {
$dependencies[] = $container->make($parameter->getClass()->name);
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
// Here we iterate through the list of declared parameters (in the method signature) and decide
// whether it should be invoked with the provided input data, or we should resolve an object
// for it (according to it's type-hint) or just call it with it's defined "default" value.
$resolvedInputData = [];
$i = 0;

foreach ($signature as $parameter) {
if (array_key_exists($parameter->name, $inputData)) {
$resolvedInputData[] = $inputData[$parameter->name];
} elseif ($class = $parameter->getClass()) {
$resolvedInputData[] = self::getInstance($inputData, $class->name, $i);
} elseif (array_key_exists($i, $inputData)) {
$resolvedInputData[] = $inputData[$i++];
} elseif ($parameter->isDefaultValueAvailable()) {
$resolvedInputData[] = $parameter->getDefaultValue();
}
}

return $resolvedInputData;
}

/**
* Determine if the given string is in Class@method syntax.
*
* @param mixed $callback
* @param string|callable $callback
* @return bool
*/
protected static function isCallableWithAtSign($callback)
{
return is_string($callback) && strpos($callback, '@') !== false;
}

/**
* @param array $inputData
* @param $className
* @param $i
* @return mixed
*/
protected static function getInstance(array $inputData, $className, &$i)
{
if (array_key_exists($className, $inputData)) {
// gets from associative array input data
return $inputData[$className];
} elseif (isset($inputData[$i]) && is_a($inputData[$i], $className)) {
// gets from indexed array input data
return $inputData[$i++];
} else {
// resolves a new instance
return self::$container->make($className);
}
}

/**
* @param ContainerContract $container
*/
public static function setContainer(ContainerContract $container)
{
self::$container = $container;
}
}
Loading