Completed
Push — master ( d92dff...55aabb )
by Boy
04:37
created

Injector::hasShare()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 1
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 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...
259
    {
260
        if ($this->isExecutable($callableOrMethodStr) === false) {
261
            throw InjectionException::fromInvalidCallable(
262
                $this->inProgressMakes,
263
                self::E_INVOKABLE,
264
                $callableOrMethodStr
265
            );
266
        }
267
268
        list(, $normalizedName) = $this->resolveAlias($name);
269
        $this->prepares[$normalizedName] = $callableOrMethodStr;
270
271
        return $this;
272
    }
273
274
    private function isExecutable($exe)
275
    {
276
        if (is_callable($exe)) {
277
            return true;
278
        }
279
        if (is_string($exe) && method_exists($exe, '__invoke')) {
280
            return true;
281
        }
282
        if (is_array($exe) && isset($exe[0], $exe[1]) && method_exists($exe[0], $exe[1])) {
283
            return true;
284
        }
285
286
        return false;
287
    }
288
289
    /**
290
     * Delegate the creation of $name instances to the specified callable
291
     *
292
     * @param string $name
293
     * @param mixed $callableOrMethodStr Any callable or provisionable invokable method
294
     * @throws ConfigException if $callableOrMethodStr is not a callable.
295
     * @return self
296
     */
297
    public function delegate($name, $callableOrMethodStr)
298
    {
299
        if ($this->isExecutable($callableOrMethodStr) === false) {
300
            $errorDetail = '';
301
            if (is_string($callableOrMethodStr)) {
302
                $errorDetail = " but received '$callableOrMethodStr'";
303
            } elseif (is_array($callableOrMethodStr) &&
304
                count($callableOrMethodStr) === 2 &&
305
                array_key_exists(0, $callableOrMethodStr) &&
306
                array_key_exists(1, $callableOrMethodStr)
307
            ) {
308
                if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) {
309
                    $errorDetail = " but received ['".$callableOrMethodStr[0]."', '".$callableOrMethodStr[1]."']";
310
                }
311
            }
312
            throw new ConfigException(
313
                sprintf(self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail),
314
                self::E_DELEGATE_ARGUMENT
315
            );
316
        }
317
        $normalizedName = $this->normalizeName($name);
318
        $this->delegates[$normalizedName] = $callableOrMethodStr;
319
320
        return $this;
321
    }
322
323
    /**
324
     * Retrieve stored data for the specified definition type
325
     *
326
     * Exposes introspection of existing binds/delegates/shares/etc for decoration and composition.
327
     *
328
     * @param string $nameFilter An optional class name filter
329
     * @param int $typeFilter A bitmask of Injector::* type constant flags
330
     * @return array
331
     */
332
    public function inspect($nameFilter = null, $typeFilter = null)
333
    {
334
        $result = [];
335
        $name = $nameFilter ? $this->normalizeName($nameFilter) : null;
336
337
        if (empty($typeFilter)) {
338
            $typeFilter = self::I_ALL;
339
        }
340
341
        $types = [
342
            self::I_BINDINGS => "classDefinitions",
343
            self::I_DELEGATES => "delegates",
344
            self::I_PREPARES => "prepares",
345
            self::I_ALIASES => "aliases",
346
            self::I_SHARES => "shares"
347
        ];
348
349
        foreach ($types as $type => $source) {
350
            if ($typeFilter & $type) {
351
                $result[$type] = $this->filter($this->{$source}, $name);
352
            }
353
        }
354
355
        return $result;
356
    }
357
358
    /**
359
     * @param $source
360
     * @param $name
361
     *
362
     * @return array
363
     */
364
    private function filter($source, $name)
365
    {
366
        if (empty($name)) {
367
            return $source;
368
        } elseif (array_key_exists($name, $source)) {
369
            return [$name => $source[$name]];
370
        } else {
371
            return [];
372
        }
373
    }
374
375
    /**
376
     * Instantiate/provision a class instance
377
     *
378
     * @param string $name
379
     * @param array $args
380
     * @throws InjectionException if a cyclic gets detected when provisioning
381
     * @return mixed
382
     */
383
    public function make($name, array $args = [])
384
    {
385
        list($className, $normalizedClass) = $this->resolveAlias($name);
386
387
        if (isset($this->inProgressMakes[$normalizedClass])) {
388
            throw new InjectionException(
389
                $this->inProgressMakes,
390
                sprintf(
391
                    self::M_CYCLIC_DEPENDENCY,
392
                    $className
393
                ),
394
                self::E_CYCLIC_DEPENDENCY
395
            );
396
        }
397
398
        $this->inProgressMakes[$normalizedClass] = count($this->inProgressMakes);
399
400
        // isset() is used specifically here because classes may be marked as "shared" before an
401
        // instance is stored. In these cases the class is "shared," but it has a null value and
402
        // instantiation is needed.
403
        if (isset($this->shares[$normalizedClass])) {
404
            unset($this->inProgressMakes[$normalizedClass]);
405
406
            return $this->shares[$normalizedClass];
407
        }
408
409
        if (isset($this->delegates[$normalizedClass])) {
410
            $executable = $this->buildExecutable($this->delegates[$normalizedClass]);
411
            $reflectionFunction = $executable->getCallableReflection();
412
            $args = $this->provisionFuncArgs($reflectionFunction, $args);
413
            $obj = call_user_func_array([$executable, '__invoke'], $args);
414
        } else {
415
            $obj = $this->provisionInstance($className, $normalizedClass, $args);
416
        }
417
418
        $obj = $this->prepareInstance($obj, $normalizedClass);
419
420
        if (array_key_exists($normalizedClass, $this->shares)) {
421
            $this->shares[$normalizedClass] = $obj;
422
        }
423
424
        unset($this->inProgressMakes[$normalizedClass]);
425
426
        return $obj;
427
    }
428
429
    /**
430
     * @param       $className
431
     * @param       $normalizedClass
432
     * @param array $definition
433
     *
434
     * @return object
435
     * @throws InjectionException
436
     */
437
    private function provisionInstance($className, $normalizedClass, array $definition)
438
    {
439
        try {
440
            $ctor = $this->reflector->getCtor($className);
441
442
            if (!$ctor) {
443
                $obj = $this->instantiateWithoutCtorParams($className);
444
            } elseif (!$ctor->isPublic()) {
445
                throw new InjectionException(
446
                    $this->inProgressMakes,
447
                    sprintf(self::M_NON_PUBLIC_CONSTRUCTOR, $className),
448
                    self::E_NON_PUBLIC_CONSTRUCTOR
449
                );
450
            } elseif ($ctorParams = $this->reflector->getCtorParams($className)) {
451
                $reflClass = $this->reflector->getClass($className);
452
                $definition = isset($this->classDefinitions[$normalizedClass])
453
                    ? array_replace($this->classDefinitions[$normalizedClass], $definition)
454
                    : $definition;
455
                $args = $this->provisionFuncArgs($ctor, $definition, $ctorParams);
456
                $obj = $reflClass->newInstanceArgs($args);
457
            } else {
458
                $obj = $this->instantiateWithoutCtorParams($className);
459
            }
460
461
            return $obj;
462
        } catch (\ReflectionException $e) {
463
            throw new InjectionException(
464
                $this->inProgressMakes,
465
                sprintf(self::M_MAKE_FAILURE, $className, $e->getMessage()),
466
                self::E_MAKE_FAILURE,
467
                $e
468
            );
469
        }
470
    }
471
472
    private function instantiateWithoutCtorParams($className)
473
    {
474
        $reflClass = $this->reflector->getClass($className);
475
476
        if (!$reflClass->isInstantiable()) {
477
            $type = $reflClass->isInterface() ? 'interface' : 'abstract class';
478
            throw new InjectionException(
479
                $this->inProgressMakes,
480
                sprintf(self::M_NEEDS_DEFINITION, $type, $className),
481
                self::E_NEEDS_DEFINITION
482
            );
483
        }
484
485
        return new $className;
486
    }
487
488
    /**
489
     * @param \ReflectionFunctionAbstract $reflFunc
490
     * @param array                       $definitions
491
     * @param array|null                  $reflParams
492
     *
493
     * @return array
494
     * @throws ConfigException
495
     * @throws InjectionException
496
     */
497
    private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definitions, array $reflParams = null)
498
    {
499
        $args = [];
500
501
        // @TODO store this in ReflectionStorage
502
        if (!isset($reflParams)) {
503
            $reflParams = $reflFunc->getParameters();
504
        }
505
506
        foreach ($reflParams as $i => $reflParam) {
507
508
            $key      = $this->isParamNameSuppliedInDefinitions($i, $reflParam, $definitions);
509
            $typeHint = ltrim($this->reflector->getParamTypeHint($reflFunc, $reflParam, array_merge($definitions, $this->paramDefinitions)), '\\');
510
            $args[$key] = null;
511
512
            foreach ($definitions as $j => $definition) {
513
514
                // interpret the param as a class name to be instantiated
515
                if (isset($definitions[$reflParam->getName()]) || array_key_exists($reflParam->getName(), $definitions)) {
516
517
                    $args[$key] = $this->make($definitions[$reflParam->getName()]);
518
                    unset($reflParams[$i], $definitions[$reflParam->getName()]);
519
520
                    continue 2;
521
                }
522
523
                // check for predefined keys first
524
                if (!is_numeric($key) && (array_key_exists($key, $definitions))) {
525
526
                    $args[$key] = $definitions[$key];
527
                    unset($reflParams[$i], $definitions[$key]);
528
529
                    continue 2;
530
                }
531
532
                // check if there is a definition set for non-type hinted parameter and if the current index is numeric
533
                // check if the type-hinted class/interface is part of the current definition
534
                if ((empty($typeHint) && is_numeric($j)) || (is_object($definition) && in_array($typeHint, $this->reflector->getImplemented(get_class($definition))))) {
535
536
                    $args[$key] = $definition;
537
                    unset($reflParams[$i], $definitions[$j]);
538
539
                    continue 2;
540
                }
541
            }
542
        }
543
544
        foreach ($reflParams as $i => $reflParam) {
545
546
            $key  = $this->isParamNameSuppliedInDefinitions($i, $reflParam, $definitions);
547
            $name = $reflParam->name;
548
549
            if (isset($definitions[$key]) || array_key_exists($key, $definitions)) {
550
                // indexed arguments take precedence over named parameters
551
                $arg = $definitions[$key];
552
            } elseif (($prefix = self::A_RAW.$name) && (isset($definitions[$prefix]) || array_key_exists($prefix, $definitions))) {
553
                // interpret the param as a raw value to be injected
554
                $arg = $definitions[$prefix];
555
            } elseif (($prefix = self::A_DELEGATE.$name) && isset($definitions[$prefix])) {
556
                // interpret the param as an invokable delegate
557
                $arg = $this->buildArgFromDelegate($name, $definitions[$prefix]);
558
            } elseif (($prefix = self::A_DEFINE.$name) && isset($definitions[$prefix])) {
559
                // interpret the param as a class definition
560
                $arg = $this->buildArgFromParamDefineArr($definitions[$prefix]);
561
            } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) {
562
                $arg = $this->buildArgFromReflParam($reflParam);
563
            }
564
565
            $args[$key] = $arg;
566
        }
567
568
        return $args;
569
    }
570
571
    /**
572
     * checks if the variable name against in the given definitions
573
     *
574
     * @param                      $i
575
     * @param \ReflectionParameter $reflParam
576
     * @param array                $definitions
577
     *
578
     * @return string
579
     */
580
    private function isParamNameSuppliedInDefinitions($i, \ReflectionParameter $reflParam, array $definitions)
581
    {
582
        $prepends = [self::A_RAW, self::A_DELEGATE, self::A_DEFINE];
583
584
        foreach ($prepends as $prepend) {
585
586
            if (array_key_exists($prepend.$reflParam->getName(), $definitions)) {
587
588
                return $prepend.$reflParam->getName();
589
            }
590
        }
591
592
        return $i;
593
    }
594
595
    private function buildArgFromParamDefineArr($definition)
596
    {
597
        if (!is_array($definition)) {
598
            throw new InjectionException(
599
                $this->inProgressMakes
600
                // @TODO Add message
601
            );
602
        }
603
604
        if (!isset($definition[0], $definition[1])) {
605
            throw new InjectionException(
606
                $this->inProgressMakes
607
                // @TODO Add message
608
            );
609
        }
610
611
        list($class, $definition) = $definition;
612
613
        return $this->make($class, $definition);
614
    }
615
616 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...
617
    {
618
        if ($this->isExecutable($callableOrMethodStr) === false) {
619
            throw InjectionException::fromInvalidCallable(
620
                $this->inProgressMakes,
621
                $callableOrMethodStr
622
            );
623
        }
624
625
        $executable = $this->buildExecutable($callableOrMethodStr);
626
627
        return $executable($paramName, $this);
628
    }
629
630
    private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam)
631
    {
632
        $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam, $this->paramDefinitions);
633
634
        if (!$typeHint) {
635
            $obj = null;
636
        } elseif ($reflParam->isDefaultValueAvailable()) {
637
            $normalizedName = $this->normalizeName($typeHint);
638
            // Injector has been told explicitly how to make this type
639
            if (isset($this->aliases[$normalizedName]) ||
640
                isset($this->delegates[$normalizedName]) ||
641
                isset($this->shares[$normalizedName])) {
642
                $obj = $this->make($typeHint);
643
            } else {
644
                $obj = $reflParam->getDefaultValue();
645
            }
646
        } else {
647
            $obj = $this->make($typeHint);
648
        }
649
650
        return $obj;
651
    }
652
653
    private function buildArgFromReflParam(\ReflectionParameter $reflParam)
654
    {
655
        if (array_key_exists($reflParam->name, $this->paramDefinitions)) {
656
            $arg = $this->paramDefinitions[$reflParam->name];
657
        } elseif ($reflParam->isDefaultValueAvailable()) {
658
            $arg = $reflParam->getDefaultValue();
659
        } elseif ($reflParam->isOptional()) {
660
            // This branch is required to work around PHP bugs where a parameter is optional
661
            // but has no default value available through reflection. Specifically, PDO exhibits
662
            // this behavior.
663
            $arg = null;
664
        } else {
665
            $reflFunc = $reflParam->getDeclaringFunction();
666
            $classWord = ($reflFunc instanceof \ReflectionMethod)
667
                ? $reflFunc->getDeclaringClass()->name.'::'
668
                : '';
669
            $funcWord = $classWord.$reflFunc->name;
670
671
            throw new InjectionException(
672
                $this->inProgressMakes,
673
                sprintf(
674
                    self::M_UNDEFINED_PARAM,
675
                    $reflParam->name,
676
                    $reflParam->getPosition(),
677
                    $funcWord
678
                ),
679
                self::E_UNDEFINED_PARAM
680
            );
681
        }
682
683
        return $arg;
684
    }
685
686
    /**
687
     * @param $obj
688
     * @param $normalizedClass
689
     *
690
     * @return mixed
691
     * @throws InjectionException
692
     */
693
    private function prepareInstance($obj, $normalizedClass)
694
    {
695
        if (isset($this->prepares[$normalizedClass])) {
696
            $prepare = $this->prepares[$normalizedClass];
697
            $executable = $this->buildExecutable($prepare);
698
            $result = $executable($obj, $this);
699
            if ($result instanceof $normalizedClass) {
700
                $obj = $result;
701
            }
702
        }
703
704
        $interfaces = @class_implements($obj);
705
706
        if ($interfaces === false) {
707
            throw new InjectionException(
708
                $this->inProgressMakes,
709
                sprintf(
710
                    self::M_MAKING_FAILED,
711
                    $normalizedClass,
712
                    gettype($obj)
713
                ),
714
                self::E_MAKING_FAILED
715
            );
716
        }
717
718
        if (empty($interfaces)) {
719
            return $obj;
720
        }
721
722
        $interfaces = array_flip(array_map([$this, 'normalizeName'], $interfaces));
723
        $prepares = array_intersect_key($this->prepares, $interfaces);
724
        foreach ($prepares as $interfaceName => $prepare) {
725
            $executable = $this->buildExecutable($prepare);
726
            $result = $executable($obj, $this);
727
            if ($result instanceof $normalizedClass) {
728
                $obj = $result;
729
            }
730
        }
731
732
        return $obj;
733
    }
734
735
    /**
736
     * Invoke the specified callable or class::method string, provisioning dependencies along the way
737
     *
738
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
739
     * @param array $args Optional array specifying params with which to invoke the provisioned callable
740
     * @throws \Atreyu\InjectionException
741
     * @return mixed Returns the invocation result returned from calling the generated executable
742
     */
743
    public function execute($callableOrMethodStr, array $args = [])
744
    {
745
        list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
746
        $executable = new Executable($reflFunc, $invocationObj);
747
        $args = $this->provisionFuncArgs($reflFunc, $args);
748
749
        return call_user_func_array([$executable, '__invoke'], $args);
750
    }
751
752
    /**
753
     * Provision an Executable instance from any valid callable or class::method string
754
     *
755
     * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string
756
     * @return \Atreyu\Executable
757
     */
758
    public function buildExecutable($callableOrMethodStr)
759
    {
760
        try {
761
            list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr);
762
        } catch (\ReflectionException $e) {
763
            throw InjectionException::fromInvalidCallable(
764
                $this->inProgressMakes,
765
                $callableOrMethodStr,
766
                $e
767
            );
768
        }
769
770
        return new Executable($reflFunc, $invocationObj);
771
    }
772
773
    private function buildExecutableStruct($callableOrMethodStr)
774
    {
775
        if (is_string($callableOrMethodStr)) {
776
            $executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr);
777
        } elseif ($callableOrMethodStr instanceof \Closure) {
778
            $callableRefl = new \ReflectionFunction($callableOrMethodStr);
779
            $executableStruct = [$callableRefl, null];
780 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...
781
            $invocationObj = $callableOrMethodStr;
782
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
783
            $executableStruct = [$callableRefl, $invocationObj];
784
        } elseif (is_array($callableOrMethodStr)
785
            && isset($callableOrMethodStr[0], $callableOrMethodStr[1])
786
            && count($callableOrMethodStr) === 2
787
        ) {
788
            $executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr);
789
        } else {
790
            throw InjectionException::fromInvalidCallable(
791
                $this->inProgressMakes,
792
                $callableOrMethodStr
793
            );
794
        }
795
796
        return $executableStruct;
797
    }
798
799
    /**
800
     * @param $stringExecutable
801
     *
802
     * @return array
803
     * @throws InjectionException
804
     */
805
    private function buildExecutableStructFromString($stringExecutable)
806
    {
807
        if (function_exists($stringExecutable)) {
808
            $callableRefl = $this->reflector->getFunction($stringExecutable);
809
            $executableStruct = [$callableRefl, null];
810 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...
811
            $invocationObj = $this->make($stringExecutable);
812
            $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke');
813
            $executableStruct = [$callableRefl, $invocationObj];
814
        } elseif (strpos($stringExecutable, '::') !== false) {
815
            list($class, $method) = explode('::', $stringExecutable, 2);
816
            $executableStruct = $this->buildStringClassMethodCallable($class, $method);
817
        } else {
818
            throw InjectionException::fromInvalidCallable(
819
                $this->inProgressMakes,
820
                $stringExecutable
821
            );
822
        }
823
824
        return $executableStruct;
825
    }
826
827
    private function buildStringClassMethodCallable($class, $method)
828
    {
829
        $relativeStaticMethodStartPos = strpos($method, 'parent::');
830
831
        if ($relativeStaticMethodStartPos === 0) {
832
            $childReflection = $this->reflector->getClass($class);
833
            $class = $childReflection->getParentClass()->name;
834
            $method = substr($method, $relativeStaticMethodStartPos + 8);
835
        }
836
837
        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...
838
        $reflectionMethod = $this->reflector->getMethod($className, $method);
839
840
        if ($reflectionMethod->isStatic()) {
841
            return [$reflectionMethod, null];
842
        }
843
844
        $instance = $this->make($className);
845
        // If the class was delegated, the instance may not be of the type
846
        // $class but some other type. We need to get the reflection on the
847
        // actual class to be able to call the method correctly.
848
        $reflectionMethod = $this->reflector->getMethod($instance, $method);
849
850
        return [$reflectionMethod, $instance];
851
    }
852
853
    private function buildExecutableStructFromArray($arrayExecutable)
854
    {
855
        list($classOrObj, $method) = $arrayExecutable;
856
857
        if (is_object($classOrObj) && method_exists($classOrObj, $method)) {
858
            $callableRefl = $this->reflector->getMethod($classOrObj, $method);
859
            $executableStruct = [$callableRefl, $classOrObj];
860
        } elseif (is_string($classOrObj)) {
861
            $executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method);
862
        } else {
863
            throw InjectionException::fromInvalidCallable(
864
                $this->inProgressMakes,
865
                $arrayExecutable
866
            );
867
        }
868
869
        return $executableStruct;
870
    }
871
}
872