Completed
Push — master ( 7ca6a8...fd7b1f )
by Rasmus
12s queued 11s
created

Container::get()   B

Complexity

Conditions 8
Paths 28

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 28
cts 28
cp 1
rs 7.9715
c 0
b 0
f 0
cc 8
nc 28
nop 1
crap 8
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
     * @var int[] map where component name => activation depth
23
     *
24
     * @see get()
25
     */
26
    private $activations = [];
27
28
    /**
29
     * @param Configuration $config
30
     */
31 1
    public function __construct(Configuration $config)
32
    {
33 1
        $config->copyTo($this);
34
35 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...
36
            [
37 1
                get_class($this)             => $this,
38 1
                __CLASS__                    => $this,
39 1
                PsrContainerInterface::class => $this,
40 1
                ContainerInterface::class    => $this,
41 1
                FactoryInterface::class      => $this,
42 1
            ];
43 1
    }
44
45
    /**
46
     * Resolve the registered component with the given name.
47
     *
48
     * @param string $name component name
49
     *
50
     * @return mixed
51
     *
52
     * @throws NotFoundException
53
     */
54 1
    public function get($name)
55
    {
56 1
        if (! isset($this->active[$name])) {
57
            try {
58 1
                if (isset($this->activations[$name])) {
59 1
                    $activations = array_flip($this->activations);
60
61 1
                    ksort($activations, SORT_NUMERIC); // order by activation depth
62
63 1
                    $activations = array_slice($activations, array_search($name, $activations, true));
64
65 1
                    $activations[] = $name;
66
67 1
                    $activation_path = implode(" -> ", $activations);
68
69 1
                    throw new ContainerException("Dependency cycle detected: " . $activation_path);
70
                }
71
72 1
                $this->activations[$name] = count($this->activations);
73
74 1
                if (isset($this->factory[$name])) {
75 1
                    $this->values[$name] = $this->call($this->factory[$name], $this->factory_map[$name]);
76 1
                } elseif (! array_key_exists($name, $this->values)) {
77 1
                    throw new NotFoundException($name);
78
                }
79
80 1
                if (isset($this->config[$name])) {
81 1
                    foreach ($this->config[$name] as $index => $config) {
82 1
                        $value = $this->call($config, [$this->values[$name]] + $this->config_map[$name][$index]);
83
84 1
                        if ($value !== null) {
85 1
                            $this->values[$name] = $value;
86 1
                        }
87 1
                    }
88 1
                }
89
90 1
                $this->active[$name] = true;
91 1
            } finally {
92 1
                unset($this->activations[$name]);
93 1
            }
94 1
        }
95
96 1
        return $this->values[$name];
97
    }
98
99
    /**
100
     * Check for the existence of a component with a given name.
101
     *
102
     * @param string $name component name
103
     *
104
     * @return bool true, if a component with the given name has been defined
105
     */
106 1
    public function has($name)
107
    {
108 1
        return array_key_exists($name, $this->values) || isset($this->factory[$name]);
109
    }
110
111
    /**
112
     * Check if a component has been unboxed and is currently active.
113
     *
114
     * @param string $name component name
115
     *
116
     * @return bool
117
     */
118 1
    public function isActive($name)
119
    {
120 1
        return isset($this->active[$name]);
121
    }
122
123
    /**
124
     * Call any given callable, using dependency injection to satisfy it's arguments, and/or
125
     * manually specifying some of those arguments - then return the value from the call.
126
     *
127
     * This will work for any callable:
128
     *
129
     *     $container->call('foo');               // function foo()
130
     *     $container->call($foo, 'baz');         // instance method $foo->baz()
131
     *     $container->call([Foo::class, 'bar']); // static method Foo::bar()
132
     *     $container->call($foo);                // closure (or class implementing __invoke)
133
     *
134
     * In any of those examples, you can also supply custom arguments, either named or
135
     * positional, or mixed, as per the `$map` argument in `register()`, `configure()`, etc.
136
     *
137
     * See also {@see create()} which lets you invoke any constructor.
138
     *
139
     * @param callable|object $callback any arbitrary closure or callable, or object implementing __invoke()
140
     * @param mixed|mixed[]   $map      mixed list/map of parameter values (and/or boxed values)
141
     *
142
     * @return mixed return value from the given callable
143
     */
144 1
    public function call($callback, $map = [])
145
    {
146 1
        $params = Reflection::createFromCallable($callback)->getParameters();
147
148 1
        return call_user_func_array($callback, $this->resolve($params, $map));
149
    }
150
151
    /**
152
     * Create an instance of a given class.
153
     *
154
     * The container will internally resolve and inject any constructor arguments
155
     * not explicitly provided in the (optional) second parameter.
156
     *
157
     * @param string        $class_name fully-qualified class-name
158
     * @param mixed|mixed[] $map        mixed list/map of parameter values (and/or boxed values)
159
     *
160
     * @return mixed
161
     *
162
     * @throws InvalidArgumentException
163
     */
164 1
    public function create($class_name, $map = [])
165
    {
166 1
        if (! class_exists($class_name)) {
167 1
            throw new InvalidArgumentException("unable to create component: {$class_name} (autoloading failed)");
168
        }
169
170 1
        $reflection = new ReflectionClass($class_name);
171
172 1
        if (! $reflection->isInstantiable()) {
173 1
            throw new InvalidArgumentException("unable to create instance of abstract class: {$class_name}");
174
        }
175
176 1
        $constructor = $reflection->getConstructor();
177
178
        $params = $constructor
179 1
            ? $this->resolve($constructor->getParameters(), $map, false)
180 1
            : [];
181
182 1
        return $reflection->newInstanceArgs($params);
183
    }
184
185
    /**
186
     * Internally resolves parameters to functions or constructors.
187
     *
188
     * This is the heart of the beast.
189
     *
190
     * @param ReflectionParameter[] $params parameter reflections
191
     * @param array                 $map    mixed list/map of parameter values (and/or boxed values)
192
     * @param bool                  $safe   if TRUE, it's considered safe to resolve against parameter names
193
     *
194
     * @return array parameters
195
     *
196
     * @throws ContainerException
197
     */
198 1
    protected function resolve(array $params, $map, $safe = true)
199
    {
200 1
        $args = [];
201
202 1
        foreach ($params as $index => $param) {
203 1
            $param_name = $param->name;
204
205 1
            if (array_key_exists($param_name, $map)) {
206 1
                $value = $map[$param_name]; // // resolve as user-provided named argument
207 1
            } elseif (array_key_exists($index, $map)) {
208 1
                $value = $map[$index]; // resolve as user-provided positional argument
209 1
            } else {
210
                // as on optimization, obtain the argument type without triggering autoload:
211
212 1
                $type = Reflection::getParameterType($param);
213
214 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...
215 1
                    $value = $map[$type]; // resolve as user-provided type-hinted argument
216 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...
217 1
                    $value = $this->get($type); // resolve as component registered by class/interface name
218 1
                } elseif ($safe && $this->has($param_name)) {
219 1
                    $value = $this->get($param_name); // resolve as component with matching parameter name
220 1
                } elseif ($param->isOptional()) {
221 1
                    $value = $param->getDefaultValue(); // unresolved, optional: resolve using default value
222 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...
223
                    $value = null; // unresolved, type-hinted, nullable: resolve as NULL
224
                } else {
225
                    // unresolved - throw a container exception:
226
227 1
                    $reflection = $param->getDeclaringFunction();
228
229 1
                    throw new ContainerException(
230 1
                        "unable to resolve parameter: \${$param_name} " . ($type ? "({$type}) " : "") .
231 1
                        "in file: " . $reflection->getFileName() . ", line " . $reflection->getStartLine()
232 1
                    );
233
                }
234
            }
235
236 1
            if ($value instanceof BoxedValueInterface) {
237 1
                $value = $value->unbox($this); // unbox a boxed value
238 1
            }
239
240 1
            $args[] = $value; // argument resolved!
241 1
        }
242
243 1
        return $args;
244
    }
245
246
    /**
247
     * Dynamically inject a component into this Container.
248
     *
249
     * Enables classes that extend `Container` to dynamically inject components (to implement "auto-wiring")
250
     *
251
     * @param string $name
252
     * @param mixed  $value
253
     */
254 1
    protected function inject($name, $value)
255
    {
256 1
        $this->values[$name] = $value;
257 1
        $this->active[$name] = true;
258 1
    }
259
}
260