<?php
namespace LottaFramework\Container;
use Closure;
use Illuminate\Contracts\Container\BindingResolutionException;
use InvalidArgumentException;
use ReflectionFunction;
use ReflectionMethod;
class BoundMethod {
/**
* Call the given Closure / class@method and inject its dependencies.
*
* @param Container $container
* @param callable|string $callback
* @param array $parameters
* @param string|null $defaultMethod
*
* @return mixed
*
* @throws \ReflectionException
* @throws \InvalidArgumentException
*/
public static function call( $container, $callback, array $parameters = [], $defaultMethod = null ) {
if ( is_string( $callback ) && ! $defaultMethod && method_exists( $callback, '__invoke' ) ) {
$defaultMethod = '__invoke';
}
if ( static::isCallableWithAtSign( $callback ) || $defaultMethod ) {
return static::callClass( $container, $callback, $parameters, $defaultMethod );
}
return static::callBoundMethod( $container, $callback, function () use ( $container, $callback, $parameters ) {
return $callback( ...array_values( static::getMethodDependencies( $container, $callback, $parameters ) ) );
} );
}
/**
* Determine if the given string is in Class@method syntax.
*
* @param mixed $callback
*
* @return bool
*/
protected static function isCallableWithAtSign( $callback ) {
return is_string( $callback ) && strpos( $callback, '@' ) !== false;
}
/**
* Call a string reference to a class using Class@method syntax.
*
* @param Container $container
* @param string $target
* @param array $parameters
* @param string|null $defaultMethod
*
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected static function callClass( $container, $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;
if ( is_null( $method ) ) {
throw new InvalidArgumentException( 'Method not provided.' );
}
return static::call(
$container, [ $container->make( $segments[0] ), $method ], $parameters
);
}
/**
* Call a method that has been bound to the container.
*
* @param Container $container
* @param callable $callback
* @param mixed $default
*
* @return mixed
*/
protected static function callBoundMethod( $container, $callback, $default ) {
if ( ! is_array( $callback ) ) {
return Util::unwrapIfClosure( $default );
}
// Here we need to turn the array callable into a Class@method string we can use to
// examine the container and see if there are any method bindings for this given
// 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] );
}
return Util::unwrapIfClosure( $default );
}
/**
* Normalize the given callback into a Class@method string.
*
* @param callable $callback
*
* @return string
*/
protected static function normalizeMethod( $callback ) {
$class = is_string( $callback[0] ) ? $callback[0] : get_class( $callback[0] );
return "{$class}@{$callback[1]}";
}
/**
* Get all dependencies for a given method.
*
* @param Container $container
* @param callable|string $callback
* @param array $parameters
*
* @return array
*
* @throws \ReflectionException
*/
protected static function getMethodDependencies( $container, $callback, array $parameters = [] ) {
$dependencies = [];
foreach ( static::getCallReflector( $callback )->getParameters() as $parameter ) {
static::addDependencyForCallParameter( $container, $parameter, $parameters, $dependencies );
}
return array_merge( $dependencies, array_values( $parameters ) );
}
/**
* Get the proper reflection instance for the given callback.
*
* @param callable|string $callback
*
* @return \ReflectionFunctionAbstract
*
* @throws \ReflectionException
*/
protected static function getCallReflector( $callback ) {
if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
$callback = explode( '::', $callback );
} elseif ( is_object( $callback ) && ! $callback instanceof Closure ) {
$callback = [ $callback, '__invoke' ];
}
return is_array( $callback )
? new ReflectionMethod( $callback[0], $callback[1] )
: new ReflectionFunction( $callback );
}
/**
* Get the dependency for the given call parameter.
*
* @param Container $container
* @param \ReflectionParameter $parameter
* @param array $parameters
* @param array $dependencies
*
* @return void
*/
protected static function addDependencyForCallParameter(
$container, $parameter,
array &$parameters, &$dependencies
) {
if ( array_key_exists( $paramName = $parameter->getName(), $parameters ) ) {
$dependencies[] = $parameters[ $paramName ];
unset( $parameters[ $paramName ] );
} elseif ( ! is_null( $className = Util::getParameterClassName( $parameter ) ) ) {
if ( array_key_exists( $className, $parameters ) ) {
$dependencies[] = $parameters[ $className ];
unset( $parameters[ $className ] );
} else {
$dependencies[] = $container->make( $className );
}
} elseif ( $parameter->isDefaultValueAvailable() ) {
$dependencies[] = $parameter->getDefaultValue();
} elseif ( ! $parameter->isOptional() && ! array_key_exists( $paramName, $parameters ) ) {
$message = "Unable to resolve dependency [{$parameter}] in class {$parameter->getDeclaringClass()->getName()}";
throw new BindingResolutionException( $message );
}
}
}
|