| 1 |
<?php |
| 2 |
|
| 3 |
namespace FL\Assistant\System\Container; |
| 4 |
|
| 5 |
class Injector { |
| 6 |
|
| 7 |
const A_RAW = ':'; |
| 8 |
const A_DELEGATE = '+'; |
| 9 |
const A_DEFINE = '@'; |
| 10 |
const I_BINDINGS = 1; |
| 11 |
const I_DELEGATES = 2; |
| 12 |
const I_PREPARES = 4; |
| 13 |
const I_ALIASES = 8; |
| 14 |
const I_SHARES = 16; |
| 15 |
const I_ALL = 31; |
| 16 |
|
| 17 |
const E_NON_EMPTY_STRING_ALIAS = 1; |
| 18 |
const M_NON_EMPTY_STRING_ALIAS = 'Invalid alias: non-empty string required at arguments 1 and 2'; |
| 19 |
const E_SHARED_CANNOT_ALIAS = 2; |
| 20 |
const M_SHARED_CANNOT_ALIAS = 'Cannot alias class %s to %s because it is currently shared'; |
| 21 |
const E_SHARE_ARGUMENT = 3; |
| 22 |
const M_SHARE_ARGUMENT = '%s::share() requires a string class name or object instance at Argument 1; %s specified'; |
| 23 |
const E_ALIASED_CANNOT_SHARE = 4; |
| 24 |
const M_ALIASED_CANNOT_SHARE = 'Cannot share class %s because it is currently aliased to %s'; |
| 25 |
const E_INVOKABLE = 5; |
| 26 |
const M_INVOKABLE = 'Invalid invokable: callable or provisional string required'; |
| 27 |
const E_NON_PUBLIC_CONSTRUCTOR = 6; |
| 28 |
const M_NON_PUBLIC_CONSTRUCTOR = 'Cannot instantiate protected/private constructor in class %s'; |
| 29 |
const E_NEEDS_DEFINITION = 7; |
| 30 |
const M_NEEDS_DEFINITION = 'Injection definition required for %s %s'; |
| 31 |
const E_MAKE_FAILURE = 8; |
| 32 |
const M_MAKE_FAILURE = 'Could not make %s: %s'; |
| 33 |
const E_UNDEFINED_PARAM = 9; |
| 34 |
const M_UNDEFINED_PARAM = 'No definition available to provision typeless parameter $%s at position %d in %s()%s'; |
| 35 |
const E_DELEGATE_ARGUMENT = 10; |
| 36 |
const M_DELEGATE_ARGUMENT = '%s::delegate expects a valid callable or executable class::method string at Argument 2%s'; |
| 37 |
const E_CYCLIC_DEPENDENCY = 11; |
| 38 |
const M_CYCLIC_DEPENDENCY = 'Detected a cyclic dependency while provisioning %s'; |
| 39 |
const E_MAKING_FAILED = 12; |
| 40 |
const M_MAKING_FAILED = "Making %s did not result in an object, instead result is of type '%s'"; |
| 41 |
|
| 42 |
private $reflector; |
| 43 |
private $classDefinitions = []; |
| 44 |
private $paramDefinitions = []; |
| 45 |
private $aliases = []; |
| 46 |
private $shares = []; |
| 47 |
private $prepares = []; |
| 48 |
private $delegates = []; |
| 49 |
private $inProgressMakes = []; |
| 50 |
|
| 51 |
public function __construct( Reflector $reflector = null ) { |
| 52 |
$this->reflector = $reflector ?: new CachingReflector; |
| 53 |
} |
| 54 |
|
| 55 |
public function __clone() { |
| 56 |
$this->inProgressMakes = []; |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Define instantiation directives for the specified class |
| 61 |
* |
| 62 |
* @param string $name The class (or alias) whose constructor arguments we wish to define |
| 63 |
* @param array $args An array mapping parameter names to values/instructions |
| 64 |
* @return self |
| 65 |
*/ |
| 66 |
public function define( $name, array $args ) { |
| 67 |
list(, $normalizedName) = $this->resolveAlias( $name ); |
| 68 |
$this->classDefinitions[ $normalizedName ] = $args; |
| 69 |
|
| 70 |
return $this; |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Assign a global default value for all parameters named $paramName |
| 75 |
* |
| 76 |
* Global parameter definitions are only used for parameters with no typehint, pre-defined or |
| 77 |
* call-time definition. |
| 78 |
* |
| 79 |
* @param string $paramName The parameter name for which this value applies |
| 80 |
* @param mixed $value The value to inject for this parameter name |
| 81 |
* @return self |
| 82 |
*/ |
| 83 |
public function defineParam( $paramName, $value ) { |
| 84 |
$this->paramDefinitions[ $paramName ] = $value; |
| 85 |
|
| 86 |
return $this; |
| 87 |
} |
| 88 |
|
| 89 |
/** |
| 90 |
* Define an alias for all occurrences of a given typehint |
| 91 |
* |
| 92 |
* Use this method to specify implementation classes for interface and abstract class typehints. |
| 93 |
* |
| 94 |
* @param string $original The typehint to replace |
| 95 |
* @param string $alias The implementation name |
| 96 |
* @throws ConfigException if any argument is empty or not a string |
| 97 |
* @return self |
| 98 |
*/ |
| 99 |
public function alias( $original, $alias ) { |
| 100 |
if ( empty( $original ) || ! is_string( $original ) ) { |
| 101 |
throw new ConfigException( |
| 102 |
self::M_NON_EMPTY_STRING_ALIAS, |
| 103 |
self::E_NON_EMPTY_STRING_ALIAS |
| 104 |
); |
| 105 |
} |
| 106 |
if ( empty( $alias ) || ! is_string( $alias ) ) { |
| 107 |
throw new ConfigException( |
| 108 |
self::M_NON_EMPTY_STRING_ALIAS, |
| 109 |
self::E_NON_EMPTY_STRING_ALIAS |
| 110 |
); |
| 111 |
} |
| 112 |
|
| 113 |
$originalNormalized = $this->normalizeName( $original ); |
| 114 |
|
| 115 |
if ( isset( $this->shares[ $originalNormalized ] ) ) { |
| 116 |
throw new ConfigException( |
| 117 |
sprintf( |
| 118 |
self::M_SHARED_CANNOT_ALIAS, |
| 119 |
$this->normalizeName( get_class( $this->shares[ $originalNormalized ] ) ), |
| 120 |
$alias |
| 121 |
), |
| 122 |
self::E_SHARED_CANNOT_ALIAS |
| 123 |
); |
| 124 |
} |
| 125 |
|
| 126 |
if ( array_key_exists( $originalNormalized, $this->shares ) ) { |
| 127 |
$aliasNormalized = $this->normalizeName( $alias ); |
| 128 |
$this->shares[ $aliasNormalized ] = null; |
| 129 |
unset( $this->shares[ $originalNormalized ] ); |
| 130 |
} |
| 131 |
|
| 132 |
$this->aliases[ $originalNormalized ] = $alias; |
| 133 |
|
| 134 |
return $this; |
| 135 |
} |
| 136 |
|
| 137 |
private function normalizeName( $className ) { |
| 138 |
return ltrim( strtolower( $className ), '\\' ); |
| 139 |
} |
| 140 |
|
| 141 |
/** |
| 142 |
* Share the specified class/instance across the Injector context |
| 143 |
* |
| 144 |
* @param mixed $nameOrInstance The class or object to share |
| 145 |
* @throws ConfigException if $nameOrInstance is not a string or an object |
| 146 |
* @return self |
| 147 |
*/ |
| 148 |
public function share( $nameOrInstance ) { |
| 149 |
if ( is_string( $nameOrInstance ) ) { |
| 150 |
$this->shareClass( $nameOrInstance ); |
| 151 |
} elseif ( is_object( $nameOrInstance ) ) { |
| 152 |
$this->shareInstance( $nameOrInstance ); |
| 153 |
} else { |
| 154 |
throw new ConfigException( |
| 155 |
sprintf( |
| 156 |
self::M_SHARE_ARGUMENT, |
| 157 |
__CLASS__, |
| 158 |
gettype( $nameOrInstance ) |
| 159 |
), |
| 160 |
self::E_SHARE_ARGUMENT |
| 161 |
); |
| 162 |
} |
| 163 |
|
| 164 |
return $this; |
| 165 |
} |
| 166 |
|
| 167 |
private function shareClass( $nameOrInstance ) { |
| 168 |
list(, $normalizedName) = $this->resolveAlias( $nameOrInstance ); |
| 169 |
$this->shares[ $normalizedName ] = isset( $this->shares[ $normalizedName ] ) |
| 170 |
? $this->shares[ $normalizedName ] |
| 171 |
: null; |
| 172 |
} |
| 173 |
|
| 174 |
private function resolveAlias( $name ) { |
| 175 |
$normalizedName = $this->normalizeName( $name ); |
| 176 |
if ( isset( $this->aliases[ $normalizedName ] ) ) { |
| 177 |
$name = $this->aliases[ $normalizedName ]; |
| 178 |
$normalizedName = $this->normalizeName( $name ); |
| 179 |
} |
| 180 |
|
| 181 |
return [ $name, $normalizedName ]; |
| 182 |
} |
| 183 |
|
| 184 |
private function shareInstance( $obj ) { |
| 185 |
$normalizedName = $this->normalizeName( get_class( $obj ) ); |
| 186 |
if ( isset( $this->aliases[ $normalizedName ] ) ) { |
| 187 |
// You cannot share an instance of a class name that is already aliased |
| 188 |
throw new ConfigException( |
| 189 |
sprintf( |
| 190 |
self::M_ALIASED_CANNOT_SHARE, |
| 191 |
$normalizedName, |
| 192 |
$this->aliases[ $normalizedName ] |
| 193 |
), |
| 194 |
self::E_ALIASED_CANNOT_SHARE |
| 195 |
); |
| 196 |
} |
| 197 |
$this->shares[ $normalizedName ] = $obj; |
| 198 |
} |
| 199 |
|
| 200 |
/** |
| 201 |
* Register a prepare callable to modify/prepare objects of type $name after instantiation |
| 202 |
* |
| 203 |
* Any callable or provisionable invokable may be specified. Preparers are passed two |
| 204 |
* arguments: the instantiated object to be mutated and the current Injector instance. |
| 205 |
* |
| 206 |
* @param string $name |
| 207 |
* @param mixed $callableOrMethodStr Any callable or provisionable invokable method |
| 208 |
* @throws InjectionException if $callableOrMethodStr is not a callable. |
| 209 |
* See https://github.com/rdlowrey/auryn#injecting-for-execution |
| 210 |
* @return self |
| 211 |
*/ |
| 212 |
public function prepare( $name, $callableOrMethodStr ) { |
| 213 |
if ( $this->isExecutable( $callableOrMethodStr ) === false ) { |
| 214 |
throw InjectionException::fromInvalidCallable( |
| 215 |
$this->inProgressMakes, |
| 216 |
$callableOrMethodStr |
| 217 |
); |
| 218 |
} |
| 219 |
|
| 220 |
list(, $normalizedName) = $this->resolveAlias( $name ); |
| 221 |
$this->prepares[ $normalizedName ] = $callableOrMethodStr; |
| 222 |
|
| 223 |
return $this; |
| 224 |
} |
| 225 |
|
| 226 |
private function isExecutable( $exe ) { |
| 227 |
if ( is_callable( $exe ) ) { |
| 228 |
return true; |
| 229 |
} |
| 230 |
if ( is_string( $exe ) && method_exists( $exe, '__invoke' ) ) { |
| 231 |
return true; |
| 232 |
} |
| 233 |
if ( is_array( $exe ) && isset( $exe[0], $exe[1] ) && method_exists( $exe[0], $exe[1] ) ) { |
| 234 |
return true; |
| 235 |
} |
| 236 |
|
| 237 |
return false; |
| 238 |
} |
| 239 |
|
| 240 |
/** |
| 241 |
* Delegate the creation of $name instances to the specified callable |
| 242 |
* |
| 243 |
* @param string $name |
| 244 |
* @param mixed $callableOrMethodStr Any callable or provisionable invokable method |
| 245 |
* @throws ConfigException if $callableOrMethodStr is not a callable. |
| 246 |
* @return self |
| 247 |
*/ |
| 248 |
public function delegate( $name, $callableOrMethodStr ) { |
| 249 |
if ( $this->isExecutable( $callableOrMethodStr ) === false ) { |
| 250 |
$errorDetail = ''; |
| 251 |
if ( is_string( $callableOrMethodStr ) ) { |
| 252 |
$errorDetail = " but received '$callableOrMethodStr'"; |
| 253 |
} elseif ( is_array( $callableOrMethodStr ) && |
| 254 |
count( $callableOrMethodStr ) === 2 && |
| 255 |
array_key_exists( 0, $callableOrMethodStr ) && |
| 256 |
array_key_exists( 1, $callableOrMethodStr ) |
| 257 |
) { |
| 258 |
if ( is_string( $callableOrMethodStr[0] ) && is_string( $callableOrMethodStr[1] ) ) { |
| 259 |
$errorDetail = " but received ['" . $callableOrMethodStr[0] . "', '" . $callableOrMethodStr[1] . "']"; |
| 260 |
} |
| 261 |
} |
| 262 |
throw new ConfigException( |
| 263 |
sprintf( self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail ), |
| 264 |
self::E_DELEGATE_ARGUMENT |
| 265 |
); |
| 266 |
} |
| 267 |
$normalizedName = $this->normalizeName( $name ); |
| 268 |
$this->delegates[ $normalizedName ] = $callableOrMethodStr; |
| 269 |
|
| 270 |
return $this; |
| 271 |
} |
| 272 |
|
| 273 |
/** |
| 274 |
* Retrieve stored data for the specified definition type |
| 275 |
* |
| 276 |
* Exposes introspection of existing binds/delegates/shares/etc for decoration and composition. |
| 277 |
* |
| 278 |
* @param string $nameFilter An optional class name filter |
| 279 |
* @param int $typeFilter A bitmask of Injector::* type constant flags |
| 280 |
* @return array |
| 281 |
*/ |
| 282 |
public function inspect( $nameFilter = null, $typeFilter = null ) { |
| 283 |
$result = []; |
| 284 |
$name = $nameFilter ? $this->normalizeName( $nameFilter ) : null; |
| 285 |
|
| 286 |
if ( empty( $typeFilter ) ) { |
| 287 |
$typeFilter = self::I_ALL; |
| 288 |
} |
| 289 |
|
| 290 |
$types = [ |
| 291 |
self::I_BINDINGS => 'classDefinitions', |
| 292 |
self::I_DELEGATES => 'delegates', |
| 293 |
self::I_PREPARES => 'prepares', |
| 294 |
self::I_ALIASES => 'aliases', |
| 295 |
self::I_SHARES => 'shares', |
| 296 |
]; |
| 297 |
|
| 298 |
foreach ( $types as $type => $source ) { |
| 299 |
if ( $typeFilter & $type ) { |
| 300 |
$result[ $type ] = $this->filter( $this->{$source}, $name ); |
| 301 |
} |
| 302 |
} |
| 303 |
|
| 304 |
return $result; |
| 305 |
} |
| 306 |
|
| 307 |
private function filter( $source, $name ) { |
| 308 |
if ( empty( $name ) ) { |
| 309 |
return $source; |
| 310 |
} elseif ( array_key_exists( $name, $source ) ) { |
| 311 |
return [ $name => $source[ $name ] ]; |
| 312 |
} else { |
| 313 |
return []; |
| 314 |
} |
| 315 |
} |
| 316 |
|
| 317 |
/** |
| 318 |
* Instantiate/provision a class instance |
| 319 |
* |
| 320 |
* @param string $name |
| 321 |
* @param array $args |
| 322 |
* @throws InjectionException if a cyclic gets detected when provisioning |
| 323 |
* @return mixed |
| 324 |
*/ |
| 325 |
public function make( $name, array $args = [] ) { |
| 326 |
list($className, $normalizedClass) = $this->resolveAlias( $name ); |
| 327 |
|
| 328 |
if ( isset( $this->inProgressMakes[ $normalizedClass ] ) ) { |
| 329 |
throw new InjectionException( |
| 330 |
$this->inProgressMakes, |
| 331 |
sprintf( |
| 332 |
self::M_CYCLIC_DEPENDENCY, |
| 333 |
$className |
| 334 |
), |
| 335 |
self::E_CYCLIC_DEPENDENCY |
| 336 |
); |
| 337 |
} |
| 338 |
|
| 339 |
$this->inProgressMakes[ $normalizedClass ] = count( $this->inProgressMakes ); |
| 340 |
|
| 341 |
// isset() is used specifically here because classes may be marked as "shared" before an |
| 342 |
// instance is stored. In these cases the class is "shared," but it has a null value and |
| 343 |
// instantiation is needed. |
| 344 |
if ( isset( $this->shares[ $normalizedClass ] ) ) { |
| 345 |
unset( $this->inProgressMakes[ $normalizedClass ] ); |
| 346 |
|
| 347 |
return $this->shares[ $normalizedClass ]; |
| 348 |
} |
| 349 |
|
| 350 |
try { |
| 351 |
if ( isset( $this->delegates[ $normalizedClass ] ) ) { |
| 352 |
$executable = $this->buildExecutable( $this->delegates[ $normalizedClass ] ); |
| 353 |
$reflectionFunction = $executable->getCallableReflection(); |
| 354 |
$args = $this->provisionFuncArgs( $reflectionFunction, $args, null, $className ); |
| 355 |
$obj = call_user_func_array( [ $executable, '__invoke' ], $args ); |
| 356 |
} else { |
| 357 |
$obj = $this->provisionInstance( $className, $normalizedClass, $args ); |
| 358 |
} |
| 359 |
|
| 360 |
$obj = $this->prepareInstance( $obj, $normalizedClass ); |
| 361 |
|
| 362 |
if ( array_key_exists( $normalizedClass, $this->shares ) ) { |
| 363 |
$this->shares[ $normalizedClass ] = $obj; |
| 364 |
} |
| 365 |
|
| 366 |
unset( $this->inProgressMakes[ $normalizedClass ] ); |
| 367 |
} catch ( \Throwable $exception ) { |
| 368 |
unset( $this->inProgressMakes[ $normalizedClass ] ); |
| 369 |
throw $exception; |
| 370 |
} catch ( \Exception $exception ) { |
| 371 |
unset( $this->inProgressMakes[ $normalizedClass ] ); |
| 372 |
throw $exception; |
| 373 |
} |
| 374 |
|
| 375 |
return $obj; |
| 376 |
} |
| 377 |
|
| 378 |
private function provisionInstance( $className, $normalizedClass, array $definition ) { |
| 379 |
try { |
| 380 |
$ctor = $this->reflector->getCtor( $className ); |
| 381 |
|
| 382 |
if ( ! $ctor ) { |
| 383 |
$obj = $this->instantiateWithoutCtorParams( $className ); |
| 384 |
} elseif ( ! $ctor->isPublic() ) { |
| 385 |
throw new InjectionException( |
| 386 |
$this->inProgressMakes, |
| 387 |
sprintf( self::M_NON_PUBLIC_CONSTRUCTOR, $className ), |
| 388 |
self::E_NON_PUBLIC_CONSTRUCTOR |
| 389 |
); |
| 390 |
} elseif ( $ctorParams = $this->reflector->getCtorParams( $className ) ) { |
| 391 |
$reflClass = $this->reflector->getClass( $className ); |
| 392 |
$definition = isset( $this->classDefinitions[ $normalizedClass ] ) |
| 393 |
? array_replace( $this->classDefinitions[ $normalizedClass ], $definition ) |
| 394 |
: $definition; |
| 395 |
$args = $this->provisionFuncArgs( $ctor, $definition, $ctorParams, $className ); |
| 396 |
$obj = $reflClass->newInstanceArgs( $args ); |
| 397 |
} else { |
| 398 |
$obj = $this->instantiateWithoutCtorParams( $className ); |
| 399 |
} |
| 400 |
|
| 401 |
return $obj; |
| 402 |
} catch ( \ReflectionException $e ) { |
| 403 |
throw new InjectionException( |
| 404 |
$this->inProgressMakes, |
| 405 |
sprintf( self::M_MAKE_FAILURE, $className, $e->getMessage() ), |
| 406 |
self::E_MAKE_FAILURE, |
| 407 |
$e |
| 408 |
); |
| 409 |
} |
| 410 |
} |
| 411 |
|
| 412 |
private function instantiateWithoutCtorParams( $className ) { |
| 413 |
$reflClass = $this->reflector->getClass( $className ); |
| 414 |
|
| 415 |
if ( ! $reflClass->isInstantiable() ) { |
| 416 |
$type = $reflClass->isInterface() ? 'interface' : 'abstract class'; |
| 417 |
throw new InjectionException( |
| 418 |
$this->inProgressMakes, |
| 419 |
sprintf( self::M_NEEDS_DEFINITION, $type, $className ), |
| 420 |
self::E_NEEDS_DEFINITION |
| 421 |
); |
| 422 |
} |
| 423 |
|
| 424 |
return new $className; |
| 425 |
} |
| 426 |
|
| 427 |
private function provisionFuncArgs( \ReflectionFunctionAbstract $reflFunc, array $definition, array $reflParams = null, $className = null ) { |
| 428 |
$args = []; |
| 429 |
|
| 430 |
// @TODO store this in ReflectionStorage |
| 431 |
if ( ! isset( $reflParams ) ) { |
| 432 |
$reflParams = $reflFunc->getParameters(); |
| 433 |
} |
| 434 |
|
| 435 |
foreach ( $reflParams as $i => $reflParam ) { |
| 436 |
$name = $reflParam->name; |
| 437 |
|
| 438 |
if ( isset( $definition[ $i ] ) || array_key_exists( $i, $definition ) ) { |
| 439 |
// indexed arguments take precedence over named parameters |
| 440 |
$arg = $definition[ $i ]; |
| 441 |
} elseif ( isset( $definition[ $name ] ) || array_key_exists( $name, $definition ) ) { |
| 442 |
// interpret the param as a class name to be instantiated |
| 443 |
$arg = $this->make( $definition[ $name ] ); |
| 444 |
} elseif ( ( $prefix = self::A_RAW . $name ) && ( isset( $definition[ $prefix ] ) || array_key_exists( $prefix, $definition ) ) ) { |
| 445 |
// interpret the param as a raw value to be injected |
| 446 |
$arg = $definition[ $prefix ]; |
| 447 |
} elseif ( ( $prefix = self::A_DELEGATE . $name ) && isset( $definition[ $prefix ] ) ) { |
| 448 |
// interpret the param as an invokable delegate |
| 449 |
$arg = $this->buildArgFromDelegate( $name, $definition[ $prefix ] ); |
| 450 |
} elseif ( ( $prefix = self::A_DEFINE . $name ) && isset( $definition[ $prefix ] ) ) { |
| 451 |
// interpret the param as a class definition |
| 452 |
$arg = $this->buildArgFromParamDefineArr( $definition[ $prefix ] ); |
| 453 |
} elseif ( ! $arg = $this->buildArgFromTypeHint( $reflFunc, $reflParam ) ) { |
| 454 |
$arg = $this->buildArgFromReflParam( $reflParam, $className ); |
| 455 |
|
| 456 |
if ( $arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic() ) { |
| 457 |
// buildArgFromReflParam might return null in case the parameter is optional |
| 458 |
// in case of variadics, the parameter is optional, but null might not be allowed |
| 459 |
continue; |
| 460 |
} |
| 461 |
} |
| 462 |
|
| 463 |
$args[] = $arg; |
| 464 |
} |
| 465 |
|
| 466 |
return $args; |
| 467 |
} |
| 468 |
|
| 469 |
private function buildArgFromParamDefineArr( $definition ) { |
| 470 |
if ( ! is_array( $definition ) ) { |
| 471 |
throw new InjectionException( |
| 472 |
$this->inProgressMakes |
| 473 |
// @TODO Add message |
| 474 |
); |
| 475 |
} |
| 476 |
|
| 477 |
if ( ! isset( $definition[0], $definition[1] ) ) { |
| 478 |
throw new InjectionException( |
| 479 |
$this->inProgressMakes |
| 480 |
// @TODO Add message |
| 481 |
); |
| 482 |
} |
| 483 |
|
| 484 |
list($class, $definition) = $definition; |
| 485 |
|
| 486 |
return $this->make( $class, $definition ); |
| 487 |
} |
| 488 |
|
| 489 |
private function buildArgFromDelegate( $paramName, $callableOrMethodStr ) { |
| 490 |
if ( $this->isExecutable( $callableOrMethodStr ) === false ) { |
| 491 |
throw InjectionException::fromInvalidCallable( |
| 492 |
$this->inProgressMakes, |
| 493 |
$callableOrMethodStr |
| 494 |
); |
| 495 |
} |
| 496 |
|
| 497 |
$executable = $this->buildExecutable( $callableOrMethodStr ); |
| 498 |
|
| 499 |
return $executable( $paramName, $this ); |
| 500 |
} |
| 501 |
|
| 502 |
private function buildArgFromTypeHint( \ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam ) { |
| 503 |
$typeHint = $this->reflector->getParamTypeHint( $reflFunc, $reflParam ); |
| 504 |
|
| 505 |
if ( ! $typeHint ) { |
| 506 |
$obj = null; |
| 507 |
} elseif ( $reflParam->isDefaultValueAvailable() ) { |
| 508 |
$normalizedName = $this->normalizeName( $typeHint ); |
| 509 |
// Injector has been told explicitly how to make this type |
| 510 |
if ( isset( $this->aliases[ $normalizedName ] ) || |
| 511 |
isset( $this->delegates[ $normalizedName ] ) || |
| 512 |
isset( $this->shares[ $normalizedName ] ) ) { |
| 513 |
$obj = $this->make( $typeHint ); |
| 514 |
} else { |
| 515 |
$obj = $reflParam->getDefaultValue(); |
| 516 |
} |
| 517 |
} else { |
| 518 |
$obj = $this->make( $typeHint ); |
| 519 |
} |
| 520 |
|
| 521 |
return $obj; |
| 522 |
} |
| 523 |
|
| 524 |
private function buildArgFromReflParam( \ReflectionParameter $reflParam, $className = null ) { |
| 525 |
if ( array_key_exists( $reflParam->name, $this->paramDefinitions ) ) { |
| 526 |
$arg = $this->paramDefinitions[ $reflParam->name ]; |
| 527 |
} elseif ( $reflParam->isDefaultValueAvailable() ) { |
| 528 |
$arg = $reflParam->getDefaultValue(); |
| 529 |
} elseif ( $reflParam->isOptional() ) { |
| 530 |
// This branch is required to work around PHP bugs where a parameter is optional |
| 531 |
// but has no default value available through reflection. Specifically, PDO exhibits |
| 532 |
// this behavior. |
| 533 |
$arg = null; |
| 534 |
} else { |
| 535 |
$reflFunc = $reflParam->getDeclaringFunction(); |
| 536 |
$classDeclare = ( $reflFunc instanceof \ReflectionMethod ) |
| 537 |
? ' declared in ' . $reflFunc->getDeclaringClass()->name . '::' |
| 538 |
: ''; |
| 539 |
$classWord = ( $reflFunc instanceof \ReflectionMethod ) |
| 540 |
? $className . '::' |
| 541 |
: ''; |
| 542 |
$funcWord = $classWord . $reflFunc->name; |
| 543 |
|
| 544 |
throw new InjectionException( |
| 545 |
$this->inProgressMakes, |
| 546 |
sprintf( |
| 547 |
self::M_UNDEFINED_PARAM, |
| 548 |
$reflParam->name, |
| 549 |
$reflParam->getPosition(), |
| 550 |
$funcWord, |
| 551 |
$classDeclare |
| 552 |
), |
| 553 |
self::E_UNDEFINED_PARAM |
| 554 |
); |
| 555 |
} |
| 556 |
|
| 557 |
return $arg; |
| 558 |
} |
| 559 |
|
| 560 |
private function prepareInstance( $obj, $normalizedClass ) { |
| 561 |
if ( isset( $this->prepares[ $normalizedClass ] ) ) { |
| 562 |
$prepare = $this->prepares[ $normalizedClass ]; |
| 563 |
$executable = $this->buildExecutable( $prepare ); |
| 564 |
$result = $executable( $obj, $this ); |
| 565 |
if ( $result instanceof $normalizedClass ) { |
| 566 |
$obj = $result; |
| 567 |
} |
| 568 |
} |
| 569 |
|
| 570 |
$interfaces = @class_implements( $obj ); |
| 571 |
|
| 572 |
if ( $interfaces === false ) { |
| 573 |
throw new InjectionException( |
| 574 |
$this->inProgressMakes, |
| 575 |
sprintf( |
| 576 |
self::M_MAKING_FAILED, |
| 577 |
$normalizedClass, |
| 578 |
gettype( $obj ) |
| 579 |
), |
| 580 |
self::E_MAKING_FAILED |
| 581 |
); |
| 582 |
} |
| 583 |
|
| 584 |
if ( empty( $interfaces ) ) { |
| 585 |
return $obj; |
| 586 |
} |
| 587 |
|
| 588 |
$interfaces = array_flip( array_map( [ $this, 'normalizeName' ], $interfaces ) ); |
| 589 |
$prepares = array_intersect_key( $this->prepares, $interfaces ); |
| 590 |
foreach ( $prepares as $interfaceName => $prepare ) { |
| 591 |
$executable = $this->buildExecutable( $prepare ); |
| 592 |
$result = $executable( $obj, $this ); |
| 593 |
if ( $result instanceof $normalizedClass ) { |
| 594 |
$obj = $result; |
| 595 |
} |
| 596 |
} |
| 597 |
|
| 598 |
return $obj; |
| 599 |
} |
| 600 |
|
| 601 |
/** |
| 602 |
* Invoke the specified callable or class::method string, provisioning dependencies along the way |
| 603 |
* |
| 604 |
* @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string |
| 605 |
* @param array $args Optional array specifying params with which to invoke the provisioned callable |
| 606 |
* @throws \Auryn\InjectionException |
| 607 |
* @return mixed Returns the invocation result returned from calling the generated executable |
| 608 |
*/ |
| 609 |
public function execute( $callableOrMethodStr, array $args = [] ) { |
| 610 |
list($reflFunc, $invocationObj) = $this->buildExecutableStruct( $callableOrMethodStr ); |
| 611 |
$executable = new Executable( $reflFunc, $invocationObj ); |
| 612 |
$args = $this->provisionFuncArgs( $reflFunc, $args, null, $invocationObj === null ? null : get_class( $invocationObj ) ); |
| 613 |
|
| 614 |
return call_user_func_array( [ $executable, '__invoke' ], $args ); |
| 615 |
} |
| 616 |
|
| 617 |
/** |
| 618 |
* Provision an Executable instance from any valid callable or class::method string |
| 619 |
* |
| 620 |
* @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string |
| 621 |
* @return \Auryn\Executable |
| 622 |
*/ |
| 623 |
public function buildExecutable( $callableOrMethodStr ) { |
| 624 |
try { |
| 625 |
list($reflFunc, $invocationObj) = $this->buildExecutableStruct( $callableOrMethodStr ); |
| 626 |
} catch ( \ReflectionException $e ) { |
| 627 |
throw InjectionException::fromInvalidCallable( |
| 628 |
$this->inProgressMakes, |
| 629 |
$callableOrMethodStr, |
| 630 |
$e |
| 631 |
); |
| 632 |
} |
| 633 |
|
| 634 |
return new Executable( $reflFunc, $invocationObj ); |
| 635 |
} |
| 636 |
|
| 637 |
private function buildExecutableStruct( $callableOrMethodStr ) { |
| 638 |
if ( is_string( $callableOrMethodStr ) ) { |
| 639 |
$executableStruct = $this->buildExecutableStructFromString( $callableOrMethodStr ); |
| 640 |
} elseif ( $callableOrMethodStr instanceof \Closure ) { |
| 641 |
$callableRefl = new \ReflectionFunction( $callableOrMethodStr ); |
| 642 |
$executableStruct = [ $callableRefl, null ]; |
| 643 |
} elseif ( is_object( $callableOrMethodStr ) && is_callable( $callableOrMethodStr ) ) { |
| 644 |
$invocationObj = $callableOrMethodStr; |
| 645 |
$callableRefl = $this->reflector->getMethod( $invocationObj, '__invoke' ); |
| 646 |
$executableStruct = [ $callableRefl, $invocationObj ]; |
| 647 |
} elseif ( is_array( $callableOrMethodStr ) |
| 648 |
&& isset( $callableOrMethodStr[0], $callableOrMethodStr[1] ) |
| 649 |
&& count( $callableOrMethodStr ) === 2 |
| 650 |
) { |
| 651 |
$executableStruct = $this->buildExecutableStructFromArray( $callableOrMethodStr ); |
| 652 |
} else { |
| 653 |
throw InjectionException::fromInvalidCallable( |
| 654 |
$this->inProgressMakes, |
| 655 |
$callableOrMethodStr |
| 656 |
); |
| 657 |
} |
| 658 |
|
| 659 |
return $executableStruct; |
| 660 |
} |
| 661 |
|
| 662 |
private function buildExecutableStructFromString( $stringExecutable ) { |
| 663 |
if ( function_exists( $stringExecutable ) ) { |
| 664 |
$callableRefl = $this->reflector->getFunction( $stringExecutable ); |
| 665 |
$executableStruct = [ $callableRefl, null ]; |
| 666 |
} elseif ( method_exists( $stringExecutable, '__invoke' ) ) { |
| 667 |
$invocationObj = $this->make( $stringExecutable ); |
| 668 |
$callableRefl = $this->reflector->getMethod( $invocationObj, '__invoke' ); |
| 669 |
$executableStruct = [ $callableRefl, $invocationObj ]; |
| 670 |
} elseif ( strpos( $stringExecutable, '::' ) !== false ) { |
| 671 |
list($class, $method) = explode( '::', $stringExecutable, 2 ); |
| 672 |
$executableStruct = $this->buildStringClassMethodCallable( $class, $method ); |
| 673 |
} else { |
| 674 |
throw InjectionException::fromInvalidCallable( |
| 675 |
$this->inProgressMakes, |
| 676 |
$stringExecutable |
| 677 |
); |
| 678 |
} |
| 679 |
|
| 680 |
return $executableStruct; |
| 681 |
} |
| 682 |
|
| 683 |
private function buildStringClassMethodCallable( $class, $method ) { |
| 684 |
$relativeStaticMethodStartPos = strpos( $method, 'parent::' ); |
| 685 |
|
| 686 |
if ( $relativeStaticMethodStartPos === 0 ) { |
| 687 |
$childReflection = $this->reflector->getClass( $class ); |
| 688 |
$class = $childReflection->getParentClass()->name; |
| 689 |
$method = substr( $method, $relativeStaticMethodStartPos + 8 ); |
| 690 |
} |
| 691 |
|
| 692 |
list($className, $normalizedClass) = $this->resolveAlias( $class ); |
| 693 |
$reflectionMethod = $this->reflector->getMethod( $className, $method ); |
| 694 |
|
| 695 |
if ( $reflectionMethod->isStatic() ) { |
| 696 |
return [ $reflectionMethod, null ]; |
| 697 |
} |
| 698 |
|
| 699 |
$instance = $this->make( $className ); |
| 700 |
// If the class was delegated, the instance may not be of the type |
| 701 |
// $class but some other type. We need to get the reflection on the |
| 702 |
// actual class to be able to call the method correctly. |
| 703 |
$reflectionMethod = $this->reflector->getMethod( $instance, $method ); |
| 704 |
|
| 705 |
return [ $reflectionMethod, $instance ]; |
| 706 |
} |
| 707 |
|
| 708 |
private function buildExecutableStructFromArray( $arrayExecutable ) { |
| 709 |
list($classOrObj, $method) = $arrayExecutable; |
| 710 |
|
| 711 |
if ( is_object( $classOrObj ) && method_exists( $classOrObj, $method ) ) { |
| 712 |
$callableRefl = $this->reflector->getMethod( $classOrObj, $method ); |
| 713 |
$executableStruct = [ $callableRefl, $classOrObj ]; |
| 714 |
} elseif ( is_string( $classOrObj ) ) { |
| 715 |
$executableStruct = $this->buildStringClassMethodCallable( $classOrObj, $method ); |
| 716 |
} else { |
| 717 |
throw InjectionException::fromInvalidCallable( |
| 718 |
$this->inProgressMakes, |
| 719 |
$arrayExecutable |
| 720 |
); |
| 721 |
} |
| 722 |
|
| 723 |
return $executableStruct; |
| 724 |
} |
| 725 |
} |
| 726 |
|