Injector::buildArgFromTypeHint()   B
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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