Completed
Push — master ( 5dba49...415dbb )
by Boy
05:54
created

Injector::isParamNameSuppliedInDefinitions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 3
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('Atreyu\Reflector', 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
    /**
459
     * @param \ReflectionFunctionAbstract $reflFunc
460
     * @param array                       $definitions
461
     * @param array|null                  $reflParams
462
     *
463
     * @return array
464
     * @throws ConfigException
465
     * @throws InjectionException
466
     */
467
    private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definitions, array $reflParams = null)
468
    {
469
        $args = [];
470
471
        // @TODO store this in ReflectionStorage
472
        if (!isset($reflParams)) {
473
            $reflParams = $reflFunc->getParameters();
474
        }
475
476
        foreach ($reflParams as $i => $reflParam) {
477
478
            $key      = $this->isParamNameSuppliedInDefinitions($i, $reflParam, $definitions);
479
            $typeHint = ltrim($this->reflector->getParamTypeHint($reflFunc, $reflParam, array_merge($definitions, $this->paramDefinitions)), '\\');
480
            $args[$key] = null;
481
482
            foreach ($definitions as $j => $definition) {
483
484
                // interpret the param as a class name to be instantiated
485
                if (isset($definitions[$reflParam->getName()]) || array_key_exists($reflParam->getName(), $definitions)) {
486
487
                    $args[$key] = $this->make($definitions[$reflParam->getName()]);
488
                    unset($reflParams[$i], $definitions[$reflParam->getName()]);
489
490
                    continue 2;
491
                }
492
493
                // check for predefined keys first
494
                if (!is_numeric($key) && (array_key_exists($key, $definitions))) {
495
496
                    $args[$key] = $definitions[$key];
497
                    unset($reflParams[$i], $definitions[$key]);
498
499
                    continue 2;
500
                }
501
502
                // check if there is a definition set for non-type hinted parameter and if the current index is numeric
503
                // check if the type-hinted class/interface is part of the current definition
504
                if ((empty($typeHint) && is_numeric($j)) || (is_object($definition) && in_array($typeHint, $this->reflector->getImplemented(get_class($definition))))) {
505
506
                    $args[$key] = $definition;
507
                    unset($reflParams[$i], $definitions[$j]);
508
509
                    continue 2;
510
                }
511
            }
512
        }
513
514
        foreach ($reflParams as $i => $reflParam) {
515
516
            $key  = $this->isParamNameSuppliedInDefinitions($i, $reflParam, $definitions);
517
            $name = $reflParam->name;
518
519
            if (isset($definitions[$key]) || array_key_exists($key, $definitions)) {
520
                // indexed arguments take precedence over named parameters
521
                $arg = $definitions[$key];
522
            } elseif (($prefix = self::A_RAW.$name) && (isset($definitions[$prefix]) || array_key_exists($prefix, $definitions))) {
523
                // interpret the param as a raw value to be injected
524
                $arg = $definitions[$prefix];
525
            } elseif (($prefix = self::A_DELEGATE.$name) && isset($definitions[$prefix])) {
526
                // interpret the param as an invokable delegate
527
                $arg = $this->buildArgFromDelegate($name, $definitions[$prefix]);
528
            } elseif (($prefix = self::A_DEFINE.$name) && isset($definitions[$prefix])) {
529
                // interpret the param as a class definition
530
                $arg = $this->buildArgFromParamDefineArr($definitions[$prefix]);
531
            } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) {
532
                $arg = $this->buildArgFromReflParam($reflParam);
533
            }
534
535
            $args[$key] = $arg;
536
        }
537
538
        return $args;
539
    }
540
541
    /**
542
     * checks if the variable name against in the given definitions
543
     *
544
     * @param                      $i
545
     * @param \ReflectionParameter $reflParam
546
     * @param array                $definitions
547
     *
548
     * @return string
549
     */
550
    private function isParamNameSuppliedInDefinitions($i, \ReflectionParameter $reflParam, array $definitions)
551
    {
552
        $prepends = [self::A_RAW, self::A_DELEGATE, self::A_DEFINE];
553
554
        foreach ($prepends as $prepend) {
555
556
            if (array_key_exists($prepend.$reflParam->getName(), $definitions)) {
557
558
                return $prepend.$reflParam->getName();
559
            }
560
        }
561
562
        return $i;
563
    }
564
565
    private function buildArgFromParamDefineArr($definition)
566
    {
567
        if (!is_array($definition)) {
568
            throw new InjectionException(
569
                $this->inProgressMakes
570
                // @TODO Add message
571
            );
572
        }
573
574
        if (!isset($definition[0], $definition[1])) {
575
            throw new InjectionException(
576
                $this->inProgressMakes
577
                // @TODO Add message
578
            );
579
        }
580
581
        list($class, $definition) = $definition;
582
583
        return $this->make($class, $definition);
584
    }
585
586 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...
587
    {
588
        if ($this->isExecutable($callableOrMethodStr) === false) {
589
            throw InjectionException::fromInvalidCallable(
590
                $this->inProgressMakes,
591
                $callableOrMethodStr
592
            );
593
        }
594
595
        $executable = $this->buildExecutable($callableOrMethodStr);
596
597
        return $executable($paramName, $this);
598
    }
599
600
    private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam)
601
    {
602
        $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam, $this->paramDefinitions);
603
604
        if (!$typeHint) {
605
            $obj = null;
606
        } elseif ($reflParam->isDefaultValueAvailable()) {
607
            $normalizedName = $this->normalizeName($typeHint);
608
            // Injector has been told explicitly how to make this type
609
            if (isset($this->aliases[$normalizedName]) ||
610
                isset($this->delegates[$normalizedName]) ||
611
                isset($this->shares[$normalizedName])) {
612
                $obj = $this->make($typeHint);
613
            } else {
614
                $obj = $reflParam->getDefaultValue();
615
            }
616
        } else {
617
            $obj = $this->make($typeHint);
618
        }
619
620
        return $obj;
621
    }
622
623
    private function buildArgFromReflParam(\ReflectionParameter $reflParam)
624
    {
625
        if (array_key_exists($reflParam->name, $this->paramDefinitions)) {
626
            $arg = $this->paramDefinitions[$reflParam->name];
627
        } elseif ($reflParam->isDefaultValueAvailable()) {
628
            $arg = $reflParam->getDefaultValue();
629
        } elseif ($reflParam->isOptional()) {
630
            // This branch is required to work around PHP bugs where a parameter is optional
631
            // but has no default value available through reflection. Specifically, PDO exhibits
632
            // this behavior.
633
            $arg = null;
634
        } else {
635
            $reflFunc = $reflParam->getDeclaringFunction();
636
            $classWord = ($reflFunc instanceof \ReflectionMethod)
637
                ? $reflFunc->getDeclaringClass()->name.'::'
638
                : '';
639
            $funcWord = $classWord.$reflFunc->name;
640
641
            throw new InjectionException(
642
                $this->inProgressMakes,
643
                sprintf(
644
                    self::M_UNDEFINED_PARAM,
645
                    $reflParam->name,
646
                    $reflParam->getPosition(),
647
                    $funcWord
648
                ),
649
                self::E_UNDEFINED_PARAM
650
            );
651
        }
652
653
        return $arg;
654
    }
655
656
    /**
657
     * @param $obj
658
     * @param $normalizedClass
659
     *
660
     * @return mixed
661
     * @throws InjectionException
662
     */
663
    private function prepareInstance($obj, $normalizedClass)
664
    {
665
        if (isset($this->prepares[$normalizedClass])) {
666
            $prepare = $this->prepares[$normalizedClass];
667
            $executable = $this->buildExecutable($prepare);
668
            $result = $executable($obj, $this);
669
            if ($result instanceof $normalizedClass) {
670
                $obj = $result;
671
            }
672
        }
673
674
        $interfaces = @class_implements($obj);
675
676
        if ($interfaces === false) {
677
            throw new InjectionException(
678
                $this->inProgressMakes,
679
                sprintf(
680
                    self::M_MAKING_FAILED,
681
                    $normalizedClass,
682
                    gettype($obj)
683
                ),
684
                self::E_MAKING_FAILED
685
            );
686
        }
687
688
        if (empty($interfaces)) {
689
            return $obj;
690
        }
691
692
        $interfaces = array_flip(array_map([$this, 'normalizeName'], $interfaces));
693
        $prepares = array_intersect_key($this->prepares, $interfaces);
694
        foreach ($prepares as $interfaceName => $prepare) {
695
            $executable = $this->buildExecutable($prepare);
696
            $result = $executable($obj, $this);
697
            if ($result instanceof $normalizedClass) {
698
                $obj = $result;
699
            }
700
        }
701
702
        return $obj;
703
    }
704
705
    /**
706
     * Invoke the specified callable or class::method string, provisioning dependencies along the way
707
     *
708
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
709
     * @param array $args Optional array specifying params with which to invoke the provisioned callable
710
     * @throws \Atreyu\InjectionException
711
     * @return mixed Returns the invocation result returned from calling the generated executable
712
     */
713
    public function execute($callableOrMethodStr, array $args = [])
714
    {
715
        list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
716
        $executable = new Executable($reflFunc, $invocationObj);
717
        $args = $this->provisionFuncArgs($reflFunc, $args);
718
719
        return call_user_func_array([$executable, '__invoke'], $args);
720
    }
721
722
    /**
723
     * Provision an Executable instance from any valid callable or class::method string
724
     *
725
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
726
     * @return \Atreyu\Executable
727
     */
728
    public function buildExecutable($callableOrMethodStr)
729
    {
730
        try {
731
            list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
732
        } catch (\ReflectionException $e) {
733
            throw InjectionException::fromInvalidCallable(
734
                $this->inProgressMakes,
735
                $callableOrMethodStr,
736
                $e
737
            );
738
        }
739
740
        return new Executable($reflFunc, $invocationObj);
741
    }
742
743
    private function buildExecutableStruct($callableOrMethodStr)
744
    {
745
        if (is_string($callableOrMethodStr)) {
746
            $executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr);
747
        } elseif ($callableOrMethodStr instanceof \Closure) {
748
            $callableRefl = new \ReflectionFunction($callableOrMethodStr);
749
            $executableStruct = [$callableRefl, null];
750 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...
751
            $invocationObj = $callableOrMethodStr;
752
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
753
            $executableStruct = [$callableRefl, $invocationObj];
754
        } elseif (is_array($callableOrMethodStr)
755
            && isset($callableOrMethodStr[0], $callableOrMethodStr[1])
756
            && count($callableOrMethodStr) === 2
757
        ) {
758
            $executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr);
759
        } else {
760
            throw InjectionException::fromInvalidCallable(
761
                $this->inProgressMakes,
762
                $callableOrMethodStr
763
            );
764
        }
765
766
        return $executableStruct;
767
    }
768
769
    /**
770
     * @param $stringExecutable
771
     *
772
     * @return array
773
     * @throws InjectionException
774
     */
775
    private function buildExecutableStructFromString($stringExecutable)
776
    {
777
        if (function_exists($stringExecutable)) {
778
            $callableRefl = $this->reflector->getFunction($stringExecutable);
779
            $executableStruct = [$callableRefl, null];
780 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...
781
            $invocationObj = $this->make($stringExecutable);
782
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
783
            $executableStruct = [$callableRefl, $invocationObj];
784
        } elseif (strpos($stringExecutable, '::') !== false) {
785
            list($class, $method) = explode('::', $stringExecutable, 2);
786
            $executableStruct = $this->buildStringClassMethodCallable($class, $method);
787
        } else {
788
            throw InjectionException::fromInvalidCallable(
789
                $this->inProgressMakes,
790
                $stringExecutable
791
            );
792
        }
793
794
        return $executableStruct;
795
    }
796
797
    private function buildStringClassMethodCallable($class, $method)
798
    {
799
        $relativeStaticMethodStartPos = strpos($method, 'parent::');
800
801
        if ($relativeStaticMethodStartPos === 0) {
802
            $childReflection = $this->reflector->getClass($class);
803
            $class = $childReflection->getParentClass()->name;
804
            $method = substr($method, $relativeStaticMethodStartPos + 8);
805
        }
806
807
        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...
808
        $reflectionMethod = $this->reflector->getMethod($className, $method);
809
810
        if ($reflectionMethod->isStatic()) {
811
            return [$reflectionMethod, null];
812
        }
813
814
        $instance = $this->make($className);
815
        // If the class was delegated, the instance may not be of the type
816
        // $class but some other type. We need to get the reflection on the
817
        // actual class to be able to call the method correctly.
818
        $reflectionMethod = $this->reflector->getMethod($instance, $method);
819
820
        return [$reflectionMethod, $instance];
821
    }
822
823
    private function buildExecutableStructFromArray($arrayExecutable)
824
    {
825
        list($classOrObj, $method) = $arrayExecutable;
826
827
        if (is_object($classOrObj) && method_exists($classOrObj, $method)) {
828
            $callableRefl = $this->reflector->getMethod($classOrObj, $method);
829
            $executableStruct = [$callableRefl, $classOrObj];
830
        } elseif (is_string($classOrObj)) {
831
            $executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method);
832
        } else {
833
            throw InjectionException::fromInvalidCallable(
834
                $this->inProgressMakes,
835
                $arrayExecutable
836
            );
837
        }
838
839
        return $executableStruct;
840
    }
841
}
842