Completed
Pull Request — master (#19)
by Rasmus
01:31
created

Container::createContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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