Container::build()   B
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 58
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 5
eloc 25
c 2
b 1
f 0
nc 5
nop 2
dl 0
loc 58
rs 8.7274

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace Spires\Container;
5
6
use Closure;
7
use ArrayAccess;
8
use ReflectionClass;
9
use ReflectionMethod;
10
use ReflectionFunction;
11
use ReflectionParameter;
12
use Spires\Contracts\Container\Container as ContainerContract;
13
use Spires\Contracts\Container\BindingResolutionException;
14
15
class Container implements ArrayAccess, ContainerContract
16
{
17
    /**
18
     * The current globally available container (if any).
19
     *
20
     * @var static
21
     */
22
    protected static $instance;
23
24
    /**
25
     * The container's bindings.
26
     *
27
     * @var array
28
     */
29
    protected $bindings = [];
30
31
    /**
32
     * The container's shared instances.
33
     *
34
     * @var array
35
     */
36
    protected $instances = [];
37
38
    /**
39
     * The stack of concretions currently being built.
40
     *
41
     * @var array
42
     */
43
    protected $buildStack = [];
44
45
    /**
46
     * Register a binding with the container.
47
     *
48
     * @param  string $abstract
49
     * @param  \Closure|string|null $concrete
50
     * @param  bool $shared
51
     * @return void
52
     */
53
    public function bind(string $abstract, $concrete = null, bool $shared = false)
54
    {
55
        $abstract = $this->normalize($abstract);
56
57
        $concrete = $this->normalize($concrete);
58
59
        // If no concrete type was given, we will simply set the concrete type to the
60
        // abstract type. After that, the concrete type to be registered as shared
61
        // without being forced to state their classes in both of the parameters.
62
        $this->dropStaleInstances($abstract);
63
64
        if (is_null($concrete)) {
65
            $concrete = $abstract;
66
        }
67
68
        // If the factory is not a Closure, it means it is just a class name which is
69
        // bound into this container to the abstract type and we will just wrap it
70
        // up inside its own Closure to give us more convenience when extending.
71
        if (!$concrete instanceof Closure) {
72
            $concrete = $this->getClosure($abstract, $concrete);
73
        }
74
75
        $this->bindings[$abstract] = compact('concrete', 'shared');
76
    }
77
78
    /**
79
     * Register a shared binding in the container.
80
     *
81
     * @param  string|array $abstract
82
     * @param  \Closure|string|null $concrete
83
     * @return void
84
     */
85
    public function singleton(string $abstract, $concrete = null)
86
    {
87
        $this->bind($abstract, $concrete, true);
0 ignored issues
show
Bug introduced by
It seems like $abstract defined by parameter $abstract on line 85 can also be of type array; however, Spires\Container\Container::bind() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
88
    }
89
90
    /**
91
     * Register an existing instance as shared in the container.
92
     *
93
     * @param  string $abstract
94
     * @param  mixed $instance
95
     * @return void
96
     */
97
    public function instance(string $abstract, $instance)
98
    {
99
        $abstract = $this->normalize($abstract);
100
101
        $this->instances[$abstract] = $instance;
102
    }
103
104
    /**
105
     * Determine if the given abstract type has been bound.
106
     *
107
     * @param  string $abstract
108
     * @return bool
109
     */
110
    public function bound(string $abstract)
111
    {
112
        $abstract = $this->normalize($abstract);
113
114
        return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]);
115
    }
116
117
    /**
118
     * Resolve the given type from the container.
119
     *
120
     * @param  string $abstract
121
     * @param  array $parameters
122
     * @return mixed
123
     */
124
    public function make(string $abstract, array $parameters = [])
125
    {
126
        $abstract = $this->normalize($abstract);
127
128
        // If an instance of the type is currently being managed as a singleton we'll
129
        // just return an existing instance instead of instantiating new instances
130
        // so the developer can keep using the same objects instance every time.
131
        if (isset($this->instances[$abstract])) {
132
            return $this->instances[$abstract];
133
        }
134
135
        $concrete = $this->getConcrete($abstract);
136
137
        // We're ready to instantiate an instance of the concrete type registered for
138
        // the binding. This will instantiate the types, as well as resolve any of
139
        // its "nested" dependencies recursively until all have gotten resolved.
140
        if ($this->isBuildable($concrete, $abstract)) {
141
            $object = $this->build($concrete, $parameters);
142
        } else {
143
            $object = $this->make($concrete, $parameters);
144
        }
145
146
        // If the requested type is registered as a singleton we'll want to cache off
147
        // the instances in "memory" so we can return it later without creating an
148
        // entirely new instance of an object on each subsequent request for it.
149
        if ($this->isShared($abstract)) {
150
            $this->instances[$abstract] = $object;
151
        }
152
153
        return $object;
154
    }
155
156
    /**
157
     * Instantiate a concrete instance of the given type.
158
     *
159
     * @param  \Closure|string $concrete
160
     * @param  array $parameters
161
     * @return mixed
162
     *
163
     * @throws \Spires\Contracts\Container\BindingResolutionException
164
     */
165
    public function build($concrete, array $parameters = [])
166
    {
167
        // If the concrete type is actually a Closure, we will just execute it and
168
        // hand back the results of the functions, which allows functions to be
169
        // used as resolvers for more fine-tuned resolution of these objects.
170
        if ($concrete instanceof Closure) {
171
            return $concrete($this, $parameters);
172
        }
173
174
        $reflector = new ReflectionClass($concrete);
175
176
        // If the type is not instantiable, the developer is attempting to resolve
177
        // an abstract type such as an Interface of Abstract Class and there is
178
        // no binding registered for the abstractions so we need to bail out.
179
        if (!$reflector->isInstantiable()) {
180
            if (!empty($this->buildStack)) {
181
                $previous = implode(', ', $this->buildStack);
182
183
                $message = "Target [$concrete] is not instantiable while building [$previous].";
184
            } else {
185
                $message = "Target [$concrete] is not instantiable.";
186
            }
187
188
            throw new BindingResolutionException($message);
189
        }
190
191
        $this->buildStack[] = $concrete;
192
193
        $constructor = $reflector->getConstructor();
194
195
        // If there are no constructors, that means there are no dependencies then
196
        // we can just resolve the instances of the objects right away, without
197
        // resolving any other types or dependencies out of these containers.
198
        if (is_null($constructor)) {
199
            array_pop($this->buildStack);
200
201
            return new $concrete;
202
        }
203
204
        $dependencies = $constructor->getParameters();
205
206
        // Once we have all the constructor's parameters we can create each of the
207
        // dependency instances and then use the reflection instances to make a
208
        // new instance of this class, injecting the created dependencies in.
209
        $parameters = $this->keyParametersByArgument(
210
            $dependencies,
211
            $parameters
212
        );
213
214
        $instances = $this->getDependencies(
215
            $dependencies,
216
            $parameters
217
        );
218
219
        array_pop($this->buildStack);
220
221
        return $reflector->newInstanceArgs($instances);
222
    }
223
224
    /**
225
     * Call the given Closure / [object, method] and inject its dependencies.
226
     *
227
     * @param  callable|array $callable
228
     * @param  array $parameters
229
     * @return mixed
230
     */
231
    public function call($callable, array $parameters = [])
232
    {
233
        $injected = $this->getInjectedMethodParameters($callable, $parameters);
234
235
        return call_user_func_array($callable, $injected);
236
    }
237
238
    /**
239
     * Set the globally available instance of the container.
240
     *
241
     * @return static
242
     */
243
    public static function getInstance()
244
    {
245
        return static::$instance;
246
    }
247
248
    /**
249
     * Set the shared instance of the container.
250
     *
251
     * @param  \Spires\Contracts\Container\Container $container
252
     * @return void
253
     */
254
    public static function setInstance(ContainerContract $container)
255
    {
256
        static::$instance = $container;
0 ignored issues
show
Documentation Bug introduced by
$container is of type object<Spires\Contracts\Container\Container>, but the property $instance was declared to be of type object<Spires\Container\Container>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
257
    }
258
259
    /**
260
     * Normalize the given class name by removing leading slashes.
261
     *
262
     * @param  mixed $service
263
     * @return mixed
264
     */
265
    protected function normalize($service)
266
    {
267
        return is_string($service) ? ltrim($service, '\\') : $service;
268
    }
269
270
    /**
271
     * Drop all of the stale instances and aliases.
272
     *
273
     * @param  string $abstract
274
     * @return void
275
     */
276
    protected function dropStaleInstances(string $abstract)
277
    {
278
        unset($this->instances[$abstract]);
279
    }
280
281
    /**
282
     * Get the Closure to be used when building a type.
283
     *
284
     * @param  string $abstract
285
     * @param  string $concrete
286
     * @return \Closure
287
     */
288
    protected function getClosure(string $abstract, string $concrete)
289
    {
290
        return function ($c, $parameters = []) use ($abstract, $concrete) {
291
            $method = ($abstract == $concrete) ? 'build' : 'make';
292
293
            return $c->$method($concrete, $parameters);
294
        };
295
    }
296
297
    /**
298
     * Get the concrete type for a given abstract.
299
     *
300
     * @param  string $abstract
301
     * @return mixed  $concrete
302
     */
303
    protected function getConcrete(string $abstract)
304
    {
305
        // If we don't have a registered resolver or concrete for the type, we'll just
306
        // assume each type is a concrete name and will attempt to resolve it as is
307
        // since the container should be able to resolve concretes automatically.
308
        if (!isset($this->bindings[$abstract])) {
309
            return $abstract;
310
        }
311
312
        return $this->bindings[$abstract]['concrete'];
313
    }
314
315
    /**
316
     * Determine if the given concrete is buildable.
317
     *
318
     * @param  mixed $concrete
319
     * @param  string $abstract
320
     * @return bool
321
     */
322
    protected function isBuildable($concrete, string $abstract)
323
    {
324
        return $concrete === $abstract || $concrete instanceof Closure;
325
    }
326
327
    /**
328
     * Determine if a given type is shared.
329
     *
330
     * @param  string $abstract
331
     * @return bool
332
     */
333
    protected function isShared(string $abstract)
334
    {
335
        $abstract = $this->normalize($abstract);
336
337
        if (isset($this->instances[$abstract])) {
338
            return true;
339
        }
340
341
        if (!isset($this->bindings[$abstract]['shared'])) {
342
            return false;
343
        }
344
345
        return $this->bindings[$abstract]['shared'] === true;
346
    }
347
348
    /**
349
     * If extra parameters are passed by numeric ID, rekey them by argument name.
350
     *
351
     * @param  array $dependencies
352
     * @param  array $parameters
353
     * @return array
354
     */
355
    protected function keyParametersByArgument(array $dependencies, array $parameters)
356
    {
357
        foreach ($parameters as $key => $value) {
358
            if (is_numeric($key)) {
359
                unset($parameters[$key]);
360
361
                $parameters[$dependencies[$key]->name] = $value;
362
            }
363
        }
364
365
        return $parameters;
366
    }
367
368
    /**
369
     * Resolve all of the dependencies from the ReflectionParameters.
370
     *
371
     * @param  array $parameters
372
     * @param  array $primitives
373
     * @return array
374
     */
375
    protected function getDependencies(array $parameters, array $primitives = [])
376
    {
377
        $dependencies = [];
378
379
        foreach ($parameters as $parameter) {
380
            $dependency = $parameter->getClass();
381
382
            // If the class is null, it means the dependency is a string or some other
383
            // primitive type which we can not resolve since it is not a class and
384
            // we will just bomb out with an error since we have no-where to go.
385
            if (array_key_exists($parameter->name, $primitives)) {
386
                $dependencies[] = $primitives[$parameter->name];
387
            } elseif (is_null($dependency)) {
388
                $dependencies[] = $this->resolveNonClass($parameter);
389
            } else {
390
                $dependencies[] = $this->resolveClass($parameter);
391
            }
392
        }
393
394
        return $dependencies;
395
    }
396
397
    /**
398
     * Resolve a non-class hinted dependency.
399
     *
400
     * @param  \ReflectionParameter $parameter
401
     * @return mixed
402
     *
403
     * @throws \Spires\Contracts\Container\BindingResolutionException
404
     */
405
    protected function resolveNonClass(ReflectionParameter $parameter)
406
    {
407
        if ($parameter->isDefaultValueAvailable()) {
408
            return $parameter->getDefaultValue();
409
        }
410
411
        $message = "Unresolvable dependency resolving [$parameter] " .
412
            "in class {$parameter->getDeclaringClass()->getName()}";
413
414
        throw new BindingResolutionException($message);
415
    }
416
417
    /**
418
     * Resolve a class based dependency from the container.
419
     *
420
     * @param  \ReflectionParameter $parameter
421
     * @return mixed
422
     *
423
     * @throws \Spires\Contracts\Container\BindingResolutionException
424
     */
425
    protected function resolveClass(ReflectionParameter $parameter)
426
    {
427
        try {
428
            return $this->make($parameter->getClass()->name);
429
        } catch (BindingResolutionException $e) {
430
            // If we can not resolve the class instance, we will check to see if the value
431
            // is optional, and if it is we will return the optional parameter value as
432
            // the value of the dependency, similarly to how we do this with scalars.
433
            if ($parameter->isOptional()) {
434
                return $parameter->getDefaultValue();
435
            }
436
437
            throw $e;
438
        }
439
    }
440
441
    /**
442
     * Get all dependencies for a given method.
443
     *
444
     * @param  callable|array $callable
445
     * @param  array $parameters
446
     * @return array
447
     */
448
    protected function getInjectedMethodParameters($callable, array $parameters = [])
449
    {
450
        $injected = [];
451
452
        foreach ($this->getCallReflector($callable)->getParameters() as $parameter) {
453
            $injected[$parameter->name] = $this->addDependencyForCallParameter($parameter, $parameters);
454
        }
455
456
        return $injected;
457
    }
458
459
    /**
460
     * Get the proper reflection instance for the given callback.
461
     *
462
     * @param  callable|array $callable
463
     * @return \ReflectionFunctionAbstract
464
     */
465
    protected function getCallReflector($callable)
466
    {
467
        if (is_array($callable)) {
468
            return new ReflectionMethod($callable[0], $callable[1]);
469
        }
470
471
        return new ReflectionFunction($callable);
472
    }
473
474
    /**
475
     * Get the dependency for the given call parameter.
476
     *
477
     * @param  \ReflectionParameter $parameter
478
     * @param  array $parameters
479
     * @return mixed
480
     */
481
    protected function addDependencyForCallParameter(ReflectionParameter $parameter, array &$parameters)
482
    {
483
        if (array_key_exists($parameter->name, $parameters)) {
484
            $value = $parameters[$parameter->name];
485
            unset($parameters[$parameter->name]);
486
            return $value;
487
        } elseif ($parameter->getClass()) {
488
            return $this->make($parameter->getClass()->name);
489
        } elseif ($parameter->isDefaultValueAvailable()) {
490
            return $parameter->getDefaultValue();
491
        }
492
493
        return array_shift($parameters);
494
    }
495
496
    /**
497
     * Determine if a given offset exists.
498
     *
499
     * @param  string $key
500
     * @return bool
501
     */
502
    public function offsetExists($key)
503
    {
504
        return $this->bound($key);
505
    }
506
507
    /**
508
     * Get the value at a given offset.
509
     *
510
     * @param  string $key
511
     * @return mixed
512
     */
513
    public function offsetGet($key)
514
    {
515
        return $this->make($key);
516
    }
517
518
    /**
519
     * Set the value at a given offset.
520
     *
521
     * @param  string $key
522
     * @param  mixed $value
523
     * @return void
524
     */
525
    public function offsetSet($key, $value)
526
    {
527
        // If the value is not a Closure, we will make it one. This simply gives
528
        // more "drop-in" replacement functionality for the Pimple which this
529
        // container's simplest functions are base modeled and built after.
530
        if (!$value instanceof Closure) {
531
            $value = function () use ($value) {
532
                return $value;
533
            };
534
        }
535
536
        $this->bind($key, $value);
537
    }
538
539
    /**
540
     * Unset the value at a given offset.
541
     *
542
     * @param  string $key
543
     * @return void
544
     */
545
    public function offsetUnset($key)
546
    {
547
        $key = $this->normalize($key);
548
549
        unset($this->bindings[$key], $this->instances[$key]);
550
    }
551
552
    /**
553
     * Dynamically access container services.
554
     *
555
     * @param  string $key
556
     * @return mixed
557
     */
558
    public function __get($key)
559
    {
560
        return $this[$key];
561
    }
562
563
    /**
564
     * Dynamically set container services.
565
     *
566
     * @param  string $key
567
     * @param  mixed $value
568
     * @return void
569
     */
570
    public function __set($key, $value)
571
    {
572
        $this[$key] = $value;
573
    }
574
}
575