Completed
Push — master ( ed37e7...f341db )
by Boy
02:01
created

Injector::delegate()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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