Completed
Push — master ( 62f8b3...047397 )
by Rasmus
14s
created

Container   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 97.75%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 6
dl 0
loc 243
ccs 87
cts 89
cp 0.9775
rs 9.3999
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B get() 0 22 4
A has() 0 4 2
A isActive() 0 4 1
A call() 0 6 1
A create() 0 20 4
A __construct() 0 13 1
C resolve() 0 47 15
A inject() 0 5 1
A initialize() 0 18 4
1
<?php
2
3
namespace mindplay\unbox;
4
5
use Interop\Container\ContainerInterface;
6
use Psr\Container\ContainerInterface as PsrContainerInterface;
7
use ReflectionClass;
8
use ReflectionFunction;
9
use ReflectionParameter;
10
11
/**
12
 * This class implements a simple dependency injection container.
13
 */
14
class Container extends Configuration implements ContainerInterface, FactoryInterface
15
{
16
    /**
17
     * @var bool[] map where component name => TRUE, if the component has been initialized
18
     */
19
    protected $active = [];
20
21
    /**
22
     * @param Configuration $config
23
     */
24 1
    public function __construct(Configuration $config)
25
    {
26 1
        $config->copyTo($this);
27
28 1
        $this->values = $this->values +
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->values + array(ge...erface::class => $this) of type array<integer|string,*> is incompatible with the declared type array<integer,*> of property $values.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
29
            [
30 1
                get_class($this)             => $this,
31 1
                __CLASS__                    => $this,
32 1
                PsrContainerInterface::class => $this,
33 1
                ContainerInterface::class    => $this,
34 1
                FactoryInterface::class      => $this,
35 1
            ];
36 1
    }
37
38
    /**
39
     * Resolve the registered component with the given name.
40
     *
41
     * @param string $name component name
42
     *
43
     * @return mixed
44
     *
45
     * @throws NotFoundException
46
     */
47 1
    public function get($name)
48
    {
49 1
        if (! isset($this->active[$name])) {
50 1
            if (isset($this->factory[$name])) {
51 1
                $factory = $this->factory[$name];
52
53 1
                $reflection = new ReflectionFunction($factory);
54
55 1
                $params = $this->resolve($reflection->getParameters(), $this->factory_map[$name]);
56
57 1
                $this->values[$name] = call_user_func_array($factory, $params);
58 1
            } elseif (! array_key_exists($name, $this->values)) {
59 1
                throw new NotFoundException($name);
60
            }
61
62 1
            $this->active[$name] = true;
63
64 1
            $this->initialize($name);
65 1
        }
66
67 1
        return $this->values[$name];
68
    }
69
70
    /**
71
     * Check for the existence of a component with a given name.
72
     *
73
     * @param string $name component name
74
     *
75
     * @return bool true, if a component with the given name has been defined
76
     */
77 1
    public function has($name)
78
    {
79 1
        return array_key_exists($name, $this->values) || isset($this->factory[$name]);
80
    }
81
82
    /**
83
     * Check if a component has been unboxed and is currently active.
84
     *
85
     * @param string $name component name
86
     *
87
     * @return bool
88
     */
89 1
    public function isActive($name)
90
    {
91 1
        return isset($this->active[$name]);
92
    }
93
94
    /**
95
     * Call any given callable, using dependency injection to satisfy it's arguments, and/or
96
     * manually specifying some of those arguments - then return the value from the call.
97
     *
98
     * This will work for any callable:
99
     *
100
     *     $container->call('foo');               // function foo()
101
     *     $container->call($foo, 'baz');         // instance method $foo->baz()
102
     *     $container->call([Foo::class, 'bar']); // static method Foo::bar()
103
     *     $container->call($foo);                // closure (or class implementing __invoke)
104
     *
105
     * In any of those examples, you can also supply custom arguments, either named or
106
     * positional, or mixed, as per the `$map` argument in `register()`, `configure()`, etc.
107
     *
108
     * See also {@see create()} which lets you invoke any constructor.
109
     *
110
     * @param callable|object $callback any arbitrary closure or callable, or object implementing __invoke()
111
     * @param mixed|mixed[]   $map      mixed list/map of parameter values (and/or boxed values)
112
     *
113
     * @return mixed return value from the given callable
114
     */
115 1
    public function call($callback, $map = [])
116
    {
117 1
        $params = Reflection::createFromCallable($callback)->getParameters();
118
119 1
        return call_user_func_array($callback, $this->resolve($params, $map));
120
    }
121
122
    /**
123
     * Create an instance of a given class.
124
     *
125
     * The container will internally resolve and inject any constructor arguments
126
     * not explicitly provided in the (optional) second parameter.
127
     *
128
     * @param string        $class_name fully-qualified class-name
129
     * @param mixed|mixed[] $map        mixed list/map of parameter values (and/or boxed values)
130
     *
131
     * @return mixed
132
     *
133
     * @throws InvalidArgumentException
134
     */
135 1
    public function create($class_name, $map = [])
136
    {
137 1
        if (! class_exists($class_name)) {
138 1
            throw new InvalidArgumentException("unable to create component: {$class_name}");
139
        }
140
141 1
        $reflection = new ReflectionClass($class_name);
142
143 1
        if (! $reflection->isInstantiable()) {
144 1
            throw new InvalidArgumentException("unable to create instance of abstract class: {$class_name}");
145
        }
146
147 1
        $constructor = $reflection->getConstructor();
148
149
        $params = $constructor
150 1
            ? $this->resolve($constructor->getParameters(), $map, false)
151 1
            : [];
152
153 1
        return $reflection->newInstanceArgs($params);
154
    }
155
156
    /**
157
     * Internally resolves parameters to functions or constructors.
158
     *
159
     * This is the heart of the beast.
160
     *
161
     * @param ReflectionParameter[] $params parameter reflections
162
     * @param array                 $map    mixed list/map of parameter values (and/or boxed values)
163
     * @param bool                  $safe   if TRUE, it's considered safe to resolve against parameter names
164
     *
165
     * @return array parameters
166
     *
167
     * @throws ContainerException
168
     */
169 1
    protected function resolve(array $params, $map, $safe = true)
170
    {
171 1
        $args = [];
172
173 1
        foreach ($params as $index => $param) {
174 1
            $param_name = $param->name;
175
176 1
            if (array_key_exists($param_name, $map)) {
177 1
                $value = $map[$param_name]; // // resolve as user-provided named argument
178 1
            } elseif (array_key_exists($index, $map)) {
179 1
                $value = $map[$index]; // resolve as user-provided positional argument
180 1
            } else {
181
                // as on optimization, obtain the argument type without triggering autoload:
182
183 1
                $type = Reflection::getParameterType($param);
184
185 1
                if ($type && isset($map[$type])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
186 1
                    $value = $map[$type]; // resolve as user-provided type-hinted argument
187 1
                } elseif ($type && $this->has($type)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
188 1
                    $value = $this->get($type); // resolve as component registered by class/interface name
189 1
                } elseif ($safe && $this->has($param_name)) {
190 1
                    $value = $this->get($param_name); // resolve as component with matching parameter name
191 1
                } elseif ($param->isOptional()) {
192 1
                    $value = $param->getDefaultValue(); // unresolved, optional: resolve using default value
193 1
                } elseif ($type && $param->allowsNull()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
194
                    $value = null; // unresolved, type-hinted, nullable: resolve as NULL
195
                } else {
196
                    // unresolved - throw a container exception:
197
198 1
                    $reflection = $param->getDeclaringFunction();
199
200 1
                    throw new ContainerException(
201 1
                        "unable to resolve parameter: \${$param_name} " . ($type ? "({$type}) " : "") .
202 1
                        "in file: " . $reflection->getFileName() . ", line " . $reflection->getStartLine()
203 1
                    );
204
                }
205
            }
206
207 1
            if ($value instanceof BoxedValueInterface) {
208 1
                $value = $value->unbox($this); // unbox a boxed value
209 1
            }
210
211 1
            $args[] = $value; // argument resolved!
212 1
        }
213
214 1
        return $args;
215
    }
216
217
    /**
218
     * Dynamically inject a component into this Container.
219
     *
220
     * Enables classes that extend `Container` to dynamically inject components (to implement "auto-wiring")
221
     *
222
     * @param string $name
223
     * @param mixed  $value
224
     */
225 1
    protected function inject($name, $value)
226
    {
227 1
        $this->values[$name] = $value;
228 1
        $this->active[$name] = true;
229 1
    }
230
231
    /**
232
     * Internally initialize an active component.
233
     *
234
     * @param string $name component name
235
     *
236
     * @return void
237
     */
238 1
    private function initialize($name)
239
    {
240 1
        if (isset($this->config[$name])) {
241 1
            foreach ($this->config[$name] as $index => $config) {
242 1
                $map = $this->config_map[$name][$index];
243
244 1
                $reflection = Reflection::createFromCallable($config);
245
246 1
                $params = $this->resolve($reflection->getParameters(), $map);
0 ignored issues
show
Documentation introduced by
$map is of type callable, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
247
248 1
                $value = call_user_func_array($config, $params);
249
250 1
                if ($value !== null) {
251 1
                    $this->values[$name] = $value;
252 1
                }
253 1
            }
254 1
        }
255 1
    }
256
}
257