PluginProbe
Assistant – Every Day Productivity Apps / 0.3.1
Assistant – Every Day Productivity Apps v0.3.1
1.5.5 trunk 0.1.0 0.2.0 0.2.1 0.2.2 0.3.0 0.3.1 0.4 0.4.1 0.4.2 0.5.0 0.5.1 0.6.0 0.7.0 0.7.0.2 0.7.0.3 0.7.0.4 0.7.0.5 0.7.0.6 0.7.0.7 0.7.0.8 0.7.0.9 0.7.1 1.0 All 71 releases
assistant / backend / src / System / Container / Injector.php

Injector.php in Assistant – Every Day Productivity Apps 0.3.1, at backend/src/System/Container/Injector.php

726 lines 23.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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