Completed
Push — master ( f341db...054038 )
by Boy
01:52
created

Injector::getReflector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Atreyu;
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 = 17;
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()";
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
    {
53
        $this->reflector = $reflector ?: new CachingReflector;
54
55
        // alias and share the reflector instance
56
        $this->alias(Reflector::class, get_class($this->reflector))->share($this->reflector);
57
    }
58
59
    public function __clone()
60
    {
61
        $this->inProgressMakes = [];
62
    }
63
64
    /**
65
     * Define instantiation directives for the specified class
66
     *
67
     * @param string $name The class (or alias) whose constructor arguments we wish to define
68
     * @param array $args An array mapping parameter names to values/instructions
69
     * @return self
70
     */
71
    public function define($name, array $args)
72
    {
73
        list(, $normalizedName) = $this->resolveAlias($name);
74
        $this->classDefinitions[$normalizedName] = $args;
75
76
        return $this;
77
    }
78
79
    /**
80
     * Assign a global default value for all parameters named $paramName
81
     *
82
     * Global parameter definitions are only used for parameters with no typehint, pre-defined or
83
     * call-time definition.
84
     *
85
     * @param string $paramName The parameter name for which this value applies
86
     * @param mixed $value The value to inject for this parameter name
87
     * @return self
88
     */
89
    public function defineParam($paramName, $value)
90
    {
91
        $this->paramDefinitions[$paramName] = $value;
92
93
        return $this;
94
    }
95
96
    /**
97
     * Define an alias for all occurrences of a given typehint
98
     *
99
     * Use this method to specify implementation classes for interface and abstract class typehints.
100
     *
101
     * @param string $original The typehint to replace
102
     * @param string $alias The implementation name
103
     * @throws ConfigException if any argument is empty or not a string
104
     * @return self
105
     */
106
    public function alias($original, $alias)
107
    {
108
        if (empty($original) || !is_string($original)) {
109
            throw new ConfigException(
110
                self::M_NON_EMPTY_STRING_ALIAS,
111
                self::E_NON_EMPTY_STRING_ALIAS
112
            );
113
        }
114
        if (empty($alias) || !is_string($alias)) {
115
            throw new ConfigException(
116
                self::M_NON_EMPTY_STRING_ALIAS,
117
                self::E_NON_EMPTY_STRING_ALIAS
118
            );
119
        }
120
121
        $originalNormalized = $this->normalizeName($original);
122
123
        if (isset($this->shares[$originalNormalized])) {
124
            throw new ConfigException(
125
                sprintf(
126
                    self::M_SHARED_CANNOT_ALIAS,
127
                    $this->normalizeName(get_class($this->shares[$originalNormalized])),
128
                    $alias
129
                ),
130
                self::E_SHARED_CANNOT_ALIAS
131
            );
132
        }
133
134
        if (array_key_exists($originalNormalized, $this->shares)) {
135
            $aliasNormalized = $this->normalizeName($alias);
136
            $this->shares[$aliasNormalized] = null;
137
            unset($this->shares[$originalNormalized]);
138
        }
139
140
        $this->aliases[$originalNormalized] = $alias;
141
142
        return $this;
143
    }
144
145
    private function normalizeName($className)
146
    {
147
        return ltrim(strtolower($className), '\\');
148
    }
149
150
    /**
151
     * Share the specified class/instance across the Injector context
152
     *
153
     * @param mixed $nameOrInstance The class or object to share
154
     * @throws ConfigException if $nameOrInstance is not a string or an object
155
     * @return self
156
     */
157
    public function share($nameOrInstance)
158
    {
159
        if (is_string($nameOrInstance)) {
160
            $this->shareClass($nameOrInstance);
161
        } elseif (is_object($nameOrInstance)) {
162
            $this->shareInstance($nameOrInstance);
163
        } else {
164
            throw new ConfigException(
165
                sprintf(
166
                    self::M_SHARE_ARGUMENT,
167
                    __CLASS__,
168
                    gettype($nameOrInstance)
169
                ),
170
                self::E_SHARE_ARGUMENT
171
            );
172
        }
173
174
        return $this;
175
    }
176
177
    /**
178
     * @param $nameOrInstance
179
     */
180
    private function shareClass($nameOrInstance)
181
    {
182
        list(, $normalizedName) = $this->resolveAlias($nameOrInstance);
183
        $this->shares[$normalizedName] = isset($this->shares[$normalizedName])
184
            ? $this->shares[$normalizedName]
185
            : null;
186
    }
187
188
    private function resolveAlias($name)
189
    {
190
        $normalizedName = $this->normalizeName($name);
191
        if (isset($this->aliases[$normalizedName])) {
192
            $name = $this->aliases[$normalizedName];
193
            $normalizedName = $this->normalizeName($name);
194
        }
195
196
        return [$name, $normalizedName];
197
    }
198
199
    private function shareInstance($obj)
200
    {
201
        $normalizedName = $this->normalizeName(get_class($obj));
202
        if (isset($this->aliases[$normalizedName])) {
203
            // You cannot share an instance of a class name that is already aliased
204
            throw new ConfigException(
205
                sprintf(
206
                    self::M_ALIASED_CANNOT_SHARE,
207
                    $normalizedName,
208
                    $this->aliases[$normalizedName]
209
                ),
210
                self::E_ALIASED_CANNOT_SHARE
211
            );
212
        }
213
        $this->shares[$normalizedName] = $obj;
214
    }
215
216
    /**
217
     * Register a prepare callable to modify/prepare objects of type $name after instantiation
218
     *
219
     * Any callable or provisionable invokable may be specified. Preparers are passed two
220
     * arguments: the instantiated object to be mutated and the current Injector instance.
221
     *
222
     * @param string $name
223
     * @param mixed $callableOrMethodStr Any callable or provisionable invokable method
224
     * @throws InjectionException if $callableOrMethodStr is not a callable.
225
     *                            See https://github.com/rdlowrey/atreyu#injecting-for-execution
226
     * @return self
227
     */
228 View Code Duplication
    public function prepare($name, $callableOrMethodStr)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
    {
230
        if ($this->isExecutable($callableOrMethodStr) === false) {
231
            throw InjectionException::fromInvalidCallable(
232
                $this->inProgressMakes,
233
                self::E_INVOKABLE,
234
                $callableOrMethodStr
235
            );
236
        }
237
238
        list(, $normalizedName) = $this->resolveAlias($name);
239
        $this->prepares[$normalizedName] = $callableOrMethodStr;
240
241
        return $this;
242
    }
243
244
    private function isExecutable($exe)
245
    {
246
        if (is_callable($exe)) {
247
            return true;
248
        }
249
        if (is_string($exe) && method_exists($exe, '__invoke')) {
250
            return true;
251
        }
252
        if (is_array($exe) && isset($exe[0], $exe[1]) && method_exists($exe[0], $exe[1])) {
253
            return true;
254
        }
255
256
        return false;
257
    }
258
259
    /**
260
     * Delegate the creation of $name instances to the specified callable
261
     *
262
     * @param string $name
263
     * @param mixed $callableOrMethodStr Any callable or provisionable invokable method
264
     * @throws ConfigException if $callableOrMethodStr is not a callable.
265
     * @return self
266
     */
267
    public function delegate($name, $callableOrMethodStr)
268
    {
269
        if ($this->isExecutable($callableOrMethodStr) === false) {
270
            $errorDetail = '';
271
            if (is_string($callableOrMethodStr)) {
272
                $errorDetail = " but received '$callableOrMethodStr'";
273
            } elseif (is_array($callableOrMethodStr) &&
274
                count($callableOrMethodStr) === 2 &&
275
                array_key_exists(0, $callableOrMethodStr) &&
276
                array_key_exists(1, $callableOrMethodStr)
277
            ) {
278
                if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) {
279
                    $errorDetail = " but received ['".$callableOrMethodStr[0]."', '".$callableOrMethodStr[1]."']";
280
                }
281
            }
282
            throw new ConfigException(
283
                sprintf(self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail),
284
                self::E_DELEGATE_ARGUMENT
285
            );
286
        }
287
        $normalizedName = $this->normalizeName($name);
288
        $this->delegates[$normalizedName] = $callableOrMethodStr;
289
290
        return $this;
291
    }
292
293
    /**
294
     * Retrieve stored data for the specified definition type
295
     *
296
     * Exposes introspection of existing binds/delegates/shares/etc for decoration and composition.
297
     *
298
     * @param string $nameFilter An optional class name filter
299
     * @param int $typeFilter A bitmask of Injector::* type constant flags
300
     * @return array
301
     */
302
    public function inspect($nameFilter = null, $typeFilter = null)
303
    {
304
        $result = [];
305
        $name = $nameFilter ? $this->normalizeName($nameFilter) : null;
306
307
        if (empty($typeFilter)) {
308
            $typeFilter = self::I_ALL;
309
        }
310
311
        $types = [
312
            self::I_BINDINGS => "classDefinitions",
313
            self::I_DELEGATES => "delegates",
314
            self::I_PREPARES => "prepares",
315
            self::I_ALIASES => "aliases",
316
            self::I_SHARES => "shares"
317
        ];
318
319
        foreach ($types as $type => $source) {
320
            if ($typeFilter & $type) {
321
                $result[$type] = $this->filter($this->{$source}, $name);
322
            }
323
        }
324
325
        return $result;
326
    }
327
328
    /**
329
     * @param $source
330
     * @param $name
331
     *
332
     * @return array
333
     */
334
    private function filter($source, $name)
335
    {
336
        if (empty($name)) {
337
            return $source;
338
        } elseif (array_key_exists($name, $source)) {
339
            return [$name => $source[$name]];
340
        } else {
341
            return [];
342
        }
343
    }
344
345
    /**
346
     * Instantiate/provision a class instance
347
     *
348
     * @param string $name
349
     * @param array $args
350
     * @throws InjectionException if a cyclic gets detected when provisioning
351
     * @return mixed
352
     */
353
    public function make($name, array $args = [])
354
    {
355
        list($className, $normalizedClass) = $this->resolveAlias($name);
356
357
        if (isset($this->inProgressMakes[$normalizedClass])) {
358
            throw new InjectionException(
359
                $this->inProgressMakes,
360
                sprintf(
361
                    self::M_CYCLIC_DEPENDENCY,
362
                    $className
363
                ),
364
                self::E_CYCLIC_DEPENDENCY
365
            );
366
        }
367
368
        $this->inProgressMakes[$normalizedClass] = count($this->inProgressMakes);
369
370
        // isset() is used specifically here because classes may be marked as "shared" before an
371
        // instance is stored. In these cases the class is "shared," but it has a null value and
372
        // instantiation is needed.
373
        if (isset($this->shares[$normalizedClass])) {
374
            unset($this->inProgressMakes[$normalizedClass]);
375
376
            return $this->shares[$normalizedClass];
377
        }
378
379
        if (isset($this->delegates[$normalizedClass])) {
380
            $executable = $this->buildExecutable($this->delegates[$normalizedClass]);
381
            $reflectionFunction = $executable->getCallableReflection();
382
            $args = $this->provisionFuncArgs($reflectionFunction, $args);
383
            $obj = call_user_func_array([$executable, '__invoke'], $args);
384
        } else {
385
            $obj = $this->provisionInstance($className, $normalizedClass, $args);
386
        }
387
388
        $obj = $this->prepareInstance($obj, $normalizedClass);
389
390
        if (array_key_exists($normalizedClass, $this->shares)) {
391
            $this->shares[$normalizedClass] = $obj;
392
        }
393
394
        unset($this->inProgressMakes[$normalizedClass]);
395
396
        return $obj;
397
    }
398
399
    /**
400
     * @param       $className
401
     * @param       $normalizedClass
402
     * @param array $definition
403
     *
404
     * @return object
405
     * @throws InjectionException
406
     */
407
    private function provisionInstance($className, $normalizedClass, array $definition)
408
    {
409
        try {
410
            $ctor = $this->reflector->getCtor($className);
411
412
            if (!$ctor) {
413
                $obj = $this->instantiateWithoutCtorParams($className);
414
            } elseif (!$ctor->isPublic()) {
415
                throw new InjectionException(
416
                    $this->inProgressMakes,
417
                    sprintf(self::M_NON_PUBLIC_CONSTRUCTOR, $className),
418
                    self::E_NON_PUBLIC_CONSTRUCTOR
419
                );
420
            } elseif ($ctorParams = $this->reflector->getCtorParams($className)) {
421
                $reflClass = $this->reflector->getClass($className);
422
                $definition = isset($this->classDefinitions[$normalizedClass])
423
                    ? array_replace($this->classDefinitions[$normalizedClass], $definition)
424
                    : $definition;
425
                $args = $this->provisionFuncArgs($ctor, $definition, $ctorParams);
426
                $obj = $reflClass->newInstanceArgs($args);
427
            } else {
428
                $obj = $this->instantiateWithoutCtorParams($className);
429
            }
430
431
            return $obj;
432
        } catch (\ReflectionException $e) {
433
            throw new InjectionException(
434
                $this->inProgressMakes,
435
                sprintf(self::M_MAKE_FAILURE, $className, $e->getMessage()),
436
                self::E_MAKE_FAILURE,
437
                $e
438
            );
439
        }
440
    }
441
442
    private function instantiateWithoutCtorParams($className)
443
    {
444
        $reflClass = $this->reflector->getClass($className);
445
446
        if (!$reflClass->isInstantiable()) {
447
            $type = $reflClass->isInterface() ? 'interface' : 'abstract class';
448
            throw new InjectionException(
449
                $this->inProgressMakes,
450
                sprintf(self::M_NEEDS_DEFINITION, $type, $className),
451
                self::E_NEEDS_DEFINITION
452
            );
453
        }
454
455
        return new $className;
456
    }
457
458
    private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definitions, array $reflParams = null)
459
    {
460
        $args = [];
461
462
        // @TODO store this in ReflectionStorage
463
        if (!isset($reflParams)) {
464
            $reflParams = $reflFunc->getParameters();
465
        }
466
467
        foreach ($reflParams as $i => $reflParam) {
468
469
            $args[$i] = null;
470
471
            foreach ($definitions as $j => $definition) {
472
473
                // use Reflection to inspect to needed parameters
474
                if (is_object($definition)
475
                    && in_array(
476
                        ltrim($this->reflector->getParamTypeHint($reflFunc, $reflParam, array_merge($definitions, $this->paramDefinitions)), '\\'),
477
                        $this->reflector->getImplemented(get_class($definition))
478
                )) {
479
                    $args[$i] = $definition;
480
481
                    // no need to use this again, if not unset, checking for a definition with numeric keys will fail later on
482
                    unset($reflParams[$i], $definitions[$j]);
483
484
                    // no need to loop again, since we found a match already!
485
                    continue 2;
486
                }
487
            }
488
        }
489
490
        foreach ($reflParams as $i => $reflParam) {
491
492
            $name = $reflParam->name;
493
494
            if (isset($definitions[$i]) || array_key_exists($i, $definitions)) {
495
                // indexed arguments take precedence over named parameters
496
                $arg = $definitions[$i];
497
            } elseif (isset($definitions[$name]) || array_key_exists($name, $definitions)) {
498
                // interpret the param as a class name to be instantiated
499
                $arg = $this->make($definitions[$name]);
500
            } elseif (($prefix = self::A_RAW.$name) && (isset($definitions[$prefix]) || array_key_exists($prefix, $definitions))) {
501
                // interpret the param as a raw value to be injected
502
                $arg = $definitions[$prefix];
503
            } elseif (($prefix = self::A_DELEGATE.$name) && isset($definitions[$prefix])) {
504
                // interpret the param as an invokable delegate
505
                $arg = $this->buildArgFromDelegate($name, $definitions[$prefix]);
506
            } elseif (($prefix = self::A_DEFINE.$name) && isset($definitions[$prefix])) {
507
                // interpret the param as a class definition
508
                $arg = $this->buildArgFromParamDefineArr($definitions[$prefix]);
509
            } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) {
510
                $arg = $this->buildArgFromReflParam($reflParam);
511
            }
512
513
            $args[$i] = $arg;
514
        }
515
516
        return $args;
517
    }
518
519
    private function buildArgFromParamDefineArr($definition)
520
    {
521
        if (!is_array($definition)) {
522
            throw new InjectionException(
523
                $this->inProgressMakes
524
                // @TODO Add message
525
            );
526
        }
527
528
        if (!isset($definition[0], $definition[1])) {
529
            throw new InjectionException(
530
                $this->inProgressMakes
531
                // @TODO Add message
532
            );
533
        }
534
535
        list($class, $definition) = $definition;
536
537
        return $this->make($class, $definition);
538
    }
539
540 View Code Duplication
    private function buildArgFromDelegate($paramName, $callableOrMethodStr)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
541
    {
542
        if ($this->isExecutable($callableOrMethodStr) === false) {
543
            throw InjectionException::fromInvalidCallable(
544
                $this->inProgressMakes,
545
                $callableOrMethodStr
546
            );
547
        }
548
549
        $executable = $this->buildExecutable($callableOrMethodStr);
550
551
        return $executable($paramName, $this);
552
    }
553
554
    private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam)
555
    {
556
        $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam, $this->paramDefinitions);
557
558
        if (!$typeHint) {
559
            $obj = null;
560
        } elseif ($reflParam->isDefaultValueAvailable()) {
561
            $normalizedName = $this->normalizeName($typeHint);
562
            // Injector has been told explicitly how to make this type
563
            if (isset($this->aliases[$normalizedName]) ||
564
                isset($this->delegates[$normalizedName]) ||
565
                isset($this->shares[$normalizedName])) {
566
                $obj = $this->make($typeHint);
567
            } else {
568
                $obj = $reflParam->getDefaultValue();
569
            }
570
        } else {
571
            $obj = $this->make($typeHint);
572
        }
573
574
        return $obj;
575
    }
576
577
    private function buildArgFromReflParam(\ReflectionParameter $reflParam)
578
    {
579
        if (array_key_exists($reflParam->name, $this->paramDefinitions)) {
580
            $arg = $this->paramDefinitions[$reflParam->name];
581
        } elseif ($reflParam->isDefaultValueAvailable()) {
582
            $arg = $reflParam->getDefaultValue();
583
        } elseif ($reflParam->isOptional()) {
584
            // This branch is required to work around PHP bugs where a parameter is optional
585
            // but has no default value available through reflection. Specifically, PDO exhibits
586
            // this behavior.
587
            $arg = null;
588
        } else {
589
            $reflFunc = $reflParam->getDeclaringFunction();
590
            $classWord = ($reflFunc instanceof \ReflectionMethod)
591
                ? $reflFunc->getDeclaringClass()->name.'::'
592
                : '';
593
            $funcWord = $classWord.$reflFunc->name;
594
595
            throw new InjectionException(
596
                $this->inProgressMakes,
597
                sprintf(
598
                    self::M_UNDEFINED_PARAM,
599
                    $reflParam->name,
600
                    $reflParam->getPosition(),
601
                    $funcWord
602
                ),
603
                self::E_UNDEFINED_PARAM
604
            );
605
        }
606
607
        return $arg;
608
    }
609
610
    /**
611
     * @param $obj
612
     * @param $normalizedClass
613
     *
614
     * @return mixed
615
     * @throws InjectionException
616
     */
617
    private function prepareInstance($obj, $normalizedClass)
618
    {
619
        if (isset($this->prepares[$normalizedClass])) {
620
            $prepare = $this->prepares[$normalizedClass];
621
            $executable = $this->buildExecutable($prepare);
622
            $result = $executable($obj, $this);
623
            if ($result instanceof $normalizedClass) {
624
                $obj = $result;
625
            }
626
        }
627
628
        $interfaces = @class_implements($obj);
629
630
        if ($interfaces === false) {
631
            throw new InjectionException(
632
                $this->inProgressMakes,
633
                sprintf(
634
                    self::M_MAKING_FAILED,
635
                    $normalizedClass,
636
                    gettype($obj)
637
                ),
638
                self::E_MAKING_FAILED
639
            );
640
        }
641
642
        if (empty($interfaces)) {
643
            return $obj;
644
        }
645
646
        $interfaces = array_flip(array_map([$this, 'normalizeName'], $interfaces));
647
        $prepares = array_intersect_key($this->prepares, $interfaces);
648
        foreach ($prepares as $interfaceName => $prepare) {
649
            $executable = $this->buildExecutable($prepare);
650
            $result = $executable($obj, $this);
651
            if ($result instanceof $normalizedClass) {
652
                $obj = $result;
653
            }
654
        }
655
656
        return $obj;
657
    }
658
659
    /**
660
     * Invoke the specified callable or class::method string, provisioning dependencies along the way
661
     *
662
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
663
     * @param array $args Optional array specifying params with which to invoke the provisioned callable
664
     * @throws \Atreyu\InjectionException
665
     * @return mixed Returns the invocation result returned from calling the generated executable
666
     */
667
    public function execute($callableOrMethodStr, array $args = [])
668
    {
669
        list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
670
        $executable = new Executable($reflFunc, $invocationObj);
671
        $args = $this->provisionFuncArgs($reflFunc, $args);
672
673
        return call_user_func_array([$executable, '__invoke'], $args);
674
    }
675
676
    /**
677
     * Provision an Executable instance from any valid callable or class::method string
678
     *
679
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
680
     * @return \Atreyu\Executable
681
     */
682
    public function buildExecutable($callableOrMethodStr)
683
    {
684
        try {
685
            list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
686
        } catch (\ReflectionException $e) {
687
            throw InjectionException::fromInvalidCallable(
688
                $this->inProgressMakes,
689
                $callableOrMethodStr,
690
                $e
691
            );
692
        }
693
694
        return new Executable($reflFunc, $invocationObj);
695
    }
696
697
    private function buildExecutableStruct($callableOrMethodStr)
698
    {
699
        if (is_string($callableOrMethodStr)) {
700
            $executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr);
701
        } elseif ($callableOrMethodStr instanceof \Closure) {
702
            $callableRefl = new \ReflectionFunction($callableOrMethodStr);
703
            $executableStruct = [$callableRefl, null];
704 View Code Duplication
        } elseif (is_object($callableOrMethodStr) && is_callable($callableOrMethodStr)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
705
            $invocationObj = $callableOrMethodStr;
706
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
707
            $executableStruct = [$callableRefl, $invocationObj];
708
        } elseif (is_array($callableOrMethodStr)
709
            && isset($callableOrMethodStr[0], $callableOrMethodStr[1])
710
            && count($callableOrMethodStr) === 2
711
        ) {
712
            $executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr);
713
        } else {
714
            throw InjectionException::fromInvalidCallable(
715
                $this->inProgressMakes,
716
                $callableOrMethodStr
717
            );
718
        }
719
720
        return $executableStruct;
721
    }
722
723
    /**
724
     * @param $stringExecutable
725
     *
726
     * @return array
727
     * @throws InjectionException
728
     */
729
    private function buildExecutableStructFromString($stringExecutable)
730
    {
731
        if (function_exists($stringExecutable)) {
732
            $callableRefl = $this->reflector->getFunction($stringExecutable);
733
            $executableStruct = [$callableRefl, null];
734 View Code Duplication
        } elseif (method_exists($stringExecutable, '__invoke')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
735
            $invocationObj = $this->make($stringExecutable);
736
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
737
            $executableStruct = [$callableRefl, $invocationObj];
738
        } elseif (strpos($stringExecutable, '::') !== false) {
739
            list($class, $method) = explode('::', $stringExecutable, 2);
740
            $executableStruct = $this->buildStringClassMethodCallable($class, $method);
741
        } else {
742
            throw InjectionException::fromInvalidCallable(
743
                $this->inProgressMakes,
744
                $stringExecutable
745
            );
746
        }
747
748
        return $executableStruct;
749
    }
750
751
    private function buildStringClassMethodCallable($class, $method)
752
    {
753
        $relativeStaticMethodStartPos = strpos($method, 'parent::');
754
755
        if ($relativeStaticMethodStartPos === 0) {
756
            $childReflection = $this->reflector->getClass($class);
757
            $class = $childReflection->getParentClass()->name;
758
            $method = substr($method, $relativeStaticMethodStartPos + 8);
759
        }
760
761
        list($className, $normalizedClass) = $this->resolveAlias($class);
0 ignored issues
show
Unused Code introduced by
The assignment to $normalizedClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
762
        $reflectionMethod = $this->reflector->getMethod($className, $method);
763
764
        if ($reflectionMethod->isStatic()) {
765
            return [$reflectionMethod, null];
766
        }
767
768
        $instance = $this->make($className);
769
        // If the class was delegated, the instance may not be of the type
770
        // $class but some other type. We need to get the reflection on the
771
        // actual class to be able to call the method correctly.
772
        $reflectionMethod = $this->reflector->getMethod($instance, $method);
773
774
        return [$reflectionMethod, $instance];
775
    }
776
777
    private function buildExecutableStructFromArray($arrayExecutable)
778
    {
779
        list($classOrObj, $method) = $arrayExecutable;
780
781
        if (is_object($classOrObj) && method_exists($classOrObj, $method)) {
782
            $callableRefl = $this->reflector->getMethod($classOrObj, $method);
783
            $executableStruct = [$callableRefl, $classOrObj];
784
        } elseif (is_string($classOrObj)) {
785
            $executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method);
786
        } else {
787
            throw InjectionException::fromInvalidCallable(
788
                $this->inProgressMakes,
789
                $arrayExecutable
790
            );
791
        }
792
793
        return $executableStruct;
794
    }
795
}
796