Completed
Push — master ( a00622...08b3e1 )
by Martin
14:53
created

Container::offsetUnset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 9.4285
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
16
class Container implements ArrayAccess, ContainerContract
17
{
18
    /**
19
     * The current globally available container (if any).
20
     *
21
     * @var static
22
     */
23
    protected static $instance;
24
25
    /**
26
     * The container's bindings.
27
     *
28
     * @var array
29
     */
30
    protected $bindings = [];
31
32
    /**
33
     * The container's shared instances.
34
     *
35
     * @var array
36
     */
37
    protected $instances = [];
38
39
    /**
40
     * The stack of concretions currently being built.
41
     *
42
     * @var array
43
     */
44
    protected $buildStack = [];
45
46
    /**
47
     * Register a binding with the container.
48
     *
49
     * @param  string $abstract
50
     * @param  \Closure|string|null $concrete
51
     * @param  bool $shared
52
     * @return void
53
     */
54
    public function bind(string $abstract, $concrete = null, bool $shared = false)
55
    {
56
        $abstract = $this->normalize($abstract);
57
58
        $concrete = $this->normalize($concrete);
59
60
        // If no concrete type was given, we will simply set the concrete type to the
61
        // abstract type. After that, the concrete type to be registered as shared
62
        // without being forced to state their classes in both of the parameters.
63
        $this->dropStaleInstances($abstract);
64
65
        if (is_null($concrete)) {
66
            $concrete = $abstract;
67
        }
68
69
        // If the factory is not a Closure, it means it is just a class name which is
70
        // bound into this container to the abstract type and we will just wrap it
71
        // up inside its own Closure to give us more convenience when extending.
72
        if (!$concrete instanceof Closure) {
73
            $concrete = $this->getClosure($abstract, $concrete);
74
        }
75
76
        $this->bindings[$abstract] = compact('concrete', 'shared');
77
    }
78
79
    /**
80
     * Register a shared binding in the container.
81
     *
82
     * @param  string|array $abstract
83
     * @param  \Closure|string|null $concrete
84
     * @return void
85
     */
86
    public function singleton(string $abstract, $concrete = null)
87
    {
88
        $this->bind($abstract, $concrete, true);
0 ignored issues
show
Bug introduced by
It seems like $abstract defined by parameter $abstract on line 86 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...
89
    }
90
91
    /**
92
     * Register an existing instance as shared in the container.
93
     *
94
     * @param  string $abstract
95
     * @param  mixed $instance
96
     * @return void
97
     */
98
    public function instance(string $abstract, $instance)
99
    {
100
        $abstract = $this->normalize($abstract);
101
102
        $this->instances[$abstract] = $instance;
103
    }
104
105
    /**
106
     * Determine if the given abstract type has been bound.
107
     *
108
     * @param  string $abstract
109
     * @return bool
110
     */
111
    public function bound(string $abstract)
112
    {
113
        $abstract = $this->normalize($abstract);
114
115
        return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]);
116
    }
117
118
    /**
119
     * Resolve the given type from the container.
120
     *
121
     * @param  string $abstract
122
     * @param  array $parameters
123
     * @return mixed
124
     */
125
    public function make(string $abstract, array $parameters = [])
126
    {
127
        $abstract = $this->normalize($abstract);
128
129
        // If an instance of the type is currently being managed as a singleton we'll
130
        // just return an existing instance instead of instantiating new instances
131
        // so the developer can keep using the same objects instance every time.
132
        if (isset($this->instances[$abstract])) {
133
            return $this->instances[$abstract];
134
        }
135
136
        $concrete = $this->getConcrete($abstract);
137
138
        // We're ready to instantiate an instance of the concrete type registered for
139
        // the binding. This will instantiate the types, as well as resolve any of
140
        // its "nested" dependencies recursively until all have gotten resolved.
141
        if ($this->isBuildable($concrete, $abstract)) {
142
            $object = $this->build($concrete, $parameters);
143
        } else {
144
            $object = $this->make($concrete, $parameters);
145
        }
146
147
        // If the requested type is registered as a singleton we'll want to cache off
148
        // the instances in "memory" so we can return it later without creating an
149
        // entirely new instance of an object on each subsequent request for it.
150
        if ($this->isShared($abstract)) {
151
            $this->instances[$abstract] = $object;
152
        }
153
154
        return $object;
155
    }
156
157
    /**
158
     * Instantiate a concrete instance of the given type.
159
     *
160
     * @param  \Closure|string $concrete
161
     * @param  array $parameters
162
     * @return mixed
163
     *
164
     * @throws \Spires\Contracts\Container\BindingResolutionException
165
     */
166
    public function build($concrete, array $parameters = [])
167
    {
168
        // If the concrete type is actually a Closure, we will just execute it and
169
        // hand back the results of the functions, which allows functions to be
170
        // used as resolvers for more fine-tuned resolution of these objects.
171
        if ($concrete instanceof Closure) {
172
            return $concrete($this, $parameters);
173
        }
174
175
        $reflector = new ReflectionClass($concrete);
176
177
        // If the type is not instantiable, the developer is attempting to resolve
178
        // an abstract type such as an Interface of Abstract Class and there is
179
        // no binding registered for the abstractions so we need to bail out.
180
        if (!$reflector->isInstantiable()) {
181
            if (!empty($this->buildStack)) {
182
                $previous = implode(', ', $this->buildStack);
183
184
                $message = "Target [$concrete] is not instantiable while building [$previous].";
185
            } else {
186
                $message = "Target [$concrete] is not instantiable.";
187
            }
188
189
            throw new BindingResolutionException($message);
190
        }
191
192
        $this->buildStack[] = $concrete;
193
194
        $constructor = $reflector->getConstructor();
195
196
        // If there are no constructors, that means there are no dependencies then
197
        // we can just resolve the instances of the objects right away, without
198
        // resolving any other types or dependencies out of these containers.
199
        if (is_null($constructor)) {
200
            array_pop($this->buildStack);
201
202
            return new $concrete;
203
        }
204
205
        $dependencies = $constructor->getParameters();
206
207
        // Once we have all the constructor's parameters we can create each of the
208
        // dependency instances and then use the reflection instances to make a
209
        // new instance of this class, injecting the created dependencies in.
210
        $parameters = $this->keyParametersByArgument(
211
            $dependencies, $parameters
212
        );
213
214
        $instances = $this->getDependencies(
215
            $dependencies, $parameters
216
        );
217
218
        array_pop($this->buildStack);
219
220
        return $reflector->newInstanceArgs($instances);
221
    }
222
223
    /**
224
     * Call the given Closure / [object, method] and inject its dependencies.
225
     *
226
     * @param  callable|array $callable
227
     * @param  array $parameters
228
     * @return mixed
229
     */
230
    public function call($callable, array $parameters = [])
231
    {
232
        $injected = $this->getInjectedMethodParameters($callable, $parameters);
233
234
        return call_user_func_array($callable, $injected);
235
    }
236
237
    /**
238
     * Set the globally available instance of the container.
239
     *
240
     * @return static
241
     */
242
    public static function getInstance()
243
    {
244
        return static::$instance;
245
    }
246
247
    /**
248
     * Set the shared instance of the container.
249
     *
250
     * @param  \Spires\Contracts\Container\Container $container
251
     * @return void
252
     */
253
    public static function setInstance(ContainerContract $container)
254
    {
255
        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...
256
    }
257
258
    /**
259
     * Normalize the given class name by removing leading slashes.
260
     *
261
     * @param  mixed $service
262
     * @return mixed
263
     */
264
    protected function normalize($service)
265
    {
266
        return is_string($service) ? ltrim($service, '\\') : $service;
267
    }
268
269
    /**
270
     * Drop all of the stale instances and aliases.
271
     *
272
     * @param  string $abstract
273
     * @return void
274
     */
275
    protected function dropStaleInstances(string $abstract)
276
    {
277
        unset($this->instances[$abstract]);
278
    }
279
280
    /**
281
     * Get the Closure to be used when building a type.
282
     *
283
     * @param  string $abstract
284
     * @param  string $concrete
285
     * @return \Closure
286
     */
287
    protected function getClosure(string $abstract, string $concrete)
288
    {
289
        return function ($c, $parameters = []) use ($abstract, $concrete) {
290
            $method = ($abstract == $concrete) ? 'build' : 'make';
291
292
            return $c->$method($concrete, $parameters);
293
        };
294
    }
295
296
    /**
297
     * Get the concrete type for a given abstract.
298
     *
299
     * @param  string $abstract
300
     * @return mixed  $concrete
301
     */
302
    protected function getConcrete(string $abstract)
303
    {
304
        // If we don't have a registered resolver or concrete for the type, we'll just
305
        // assume each type is a concrete name and will attempt to resolve it as is
306
        // since the container should be able to resolve concretes automatically.
307
        if (!isset($this->bindings[$abstract])) {
308
            return $abstract;
309
        }
310
311
        return $this->bindings[$abstract]['concrete'];
312
    }
313
314
    /**
315
     * Determine if the given concrete is buildable.
316
     *
317
     * @param  mixed $concrete
318
     * @param  string $abstract
319
     * @return bool
320
     */
321
    protected function isBuildable($concrete, string $abstract)
322
    {
323
        return $concrete === $abstract || $concrete instanceof Closure;
324
    }
325
326
    /**
327
     * Determine if a given type is shared.
328
     *
329
     * @param  string $abstract
330
     * @return bool
331
     */
332
    protected function isShared(string $abstract)
333
    {
334
        $abstract = $this->normalize($abstract);
335
336
        if (isset($this->instances[$abstract])) {
337
            return true;
338
        }
339
340
        if (!isset($this->bindings[$abstract]['shared'])) {
341
            return false;
342
        }
343
344
        return $this->bindings[$abstract]['shared'] === true;
345
    }
346
347
    /**
348
     * If extra parameters are passed by numeric ID, rekey them by argument name.
349
     *
350
     * @param  array $dependencies
351
     * @param  array $parameters
352
     * @return array
353
     */
354
    protected function keyParametersByArgument(array $dependencies, array $parameters)
355
    {
356
        foreach ($parameters as $key => $value) {
357
            if (is_numeric($key)) {
358
                unset($parameters[$key]);
359
360
                $parameters[$dependencies[$key]->name] = $value;
361
            }
362
        }
363
364
        return $parameters;
365
    }
366
367
    /**
368
     * Resolve all of the dependencies from the ReflectionParameters.
369
     *
370
     * @param  array $parameters
371
     * @param  array $primitives
372
     * @return array
373
     */
374
    protected function getDependencies(array $parameters, array $primitives = [])
375
    {
376
        $dependencies = [];
377
378
        foreach ($parameters as $parameter) {
379
            $dependency = $parameter->getClass();
380
381
            // If the class is null, it means the dependency is a string or some other
382
            // primitive type which we can not resolve since it is not a class and
383
            // we will just bomb out with an error since we have no-where to go.
384
            if (array_key_exists($parameter->name, $primitives)) {
385
                $dependencies[] = $primitives[$parameter->name];
386
            } elseif (is_null($dependency)) {
387
                $dependencies[] = $this->resolveNonClass($parameter);
388
            } else {
389
                $dependencies[] = $this->resolveClass($parameter);
390
            }
391
        }
392
393
        return $dependencies;
394
    }
395
396
    /**
397
     * Resolve a non-class hinted dependency.
398
     *
399
     * @param  \ReflectionParameter $parameter
400
     * @return mixed
401
     *
402
     * @throws \Spires\Contracts\Container\BindingResolutionException
403
     */
404
    protected function resolveNonClass(ReflectionParameter $parameter)
405
    {
406
        if ($parameter->isDefaultValueAvailable()) {
407
            return $parameter->getDefaultValue();
408
        }
409
410
        $message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";
411
412
        throw new BindingResolutionException($message);
413
    }
414
415
    /**
416
     * Resolve a class based dependency from the container.
417
     *
418
     * @param  \ReflectionParameter $parameter
419
     * @return mixed
420
     *
421
     * @throws \Spires\Contracts\Container\BindingResolutionException
422
     */
423
    protected function resolveClass(ReflectionParameter $parameter)
424
    {
425
        try {
426
            return $this->make($parameter->getClass()->name);
427
        }
428
429
            // If we can not resolve the class instance, we will check to see if the value
430
            // is optional, and if it is we will return the optional parameter value as
431
            // the value of the dependency, similarly to how we do this with scalars.
432
        catch (BindingResolutionException $e) {
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
    /**
476
     * Get the dependency for the given call parameter.
477
     *
478
     * @param  \ReflectionParameter $parameter
479
     * @param  array $parameters
480
     * @return mixed
481
     */
482
    protected function addDependencyForCallParameter(ReflectionParameter $parameter, array &$parameters)
483
    {
484
        if (array_key_exists($parameter->name, $parameters)) {
485
            $value = $parameters[$parameter->name];
486
            unset($parameters[$parameter->name]);
487
            return $value;
488
        } elseif ($parameter->getClass()) {
489
            return $this->make($parameter->getClass()->name);
490
        } elseif ($parameter->isDefaultValueAvailable()) {
491
            return $parameter->getDefaultValue();
492
        }
493
494
        return array_shift($parameters);
495
    }
496
497
    /**
498
     * Determine if a given offset exists.
499
     *
500
     * @param  string $key
501
     * @return bool
502
     */
503
    public function offsetExists($key)
504
    {
505
        return $this->bound($key);
506
    }
507
508
    /**
509
     * Get the value at a given offset.
510
     *
511
     * @param  string $key
512
     * @return mixed
513
     */
514
    public function offsetGet($key)
515
    {
516
        return $this->make($key);
517
    }
518
519
    /**
520
     * Set the value at a given offset.
521
     *
522
     * @param  string $key
523
     * @param  mixed $value
524
     * @return void
525
     */
526
    public function offsetSet($key, $value)
527
    {
528
        // If the value is not a Closure, we will make it one. This simply gives
529
        // more "drop-in" replacement functionality for the Pimple which this
530
        // container's simplest functions are base modeled and built after.
531
        if (!$value instanceof Closure) {
532
            $value = function () use ($value) {
533
                return $value;
534
            };
535
        }
536
537
        $this->bind($key, $value);
538
    }
539
540
    /**
541
     * Unset the value at a given offset.
542
     *
543
     * @param  string $key
544
     * @return void
545
     */
546
    public function offsetUnset($key)
547
    {
548
        $key = $this->normalize($key);
549
550
        unset($this->bindings[$key], $this->instances[$key]);
551
    }
552
553
    /**
554
     * Dynamically access container services.
555
     *
556
     * @param  string $key
557
     * @return mixed
558
     */
559
    public function __get($key)
560
    {
561
        return $this[$key];
562
    }
563
564
    /**
565
     * Dynamically set container services.
566
     *
567
     * @param  string $key
568
     * @param  mixed $value
569
     * @return void
570
     */
571
    public function __set($key, $value)
572
    {
573
        $this[$key] = $value;
574
    }
575
}
576