Completed
Pull Request — master (#9)
by Rasmus
02:22
created

ContainerFactory::import()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
ccs 11
cts 11
cp 1
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2
1
<?php
2
3
namespace mindplay\unbox;
4
5
use Closure;
6
use InvalidArgumentException;
7
use ReflectionParameter;
8
9
/**
10
 * This class provides boostrapping/configuration facilities for creation of `Container` instances.
11
 */
12
class ContainerFactory extends Configuration
13
{
14 1
    public function __construct()
15 1
    {}
16
17
    /**
18
     * Register a component for dependency injection.
19
     *
20
     * There are numerous valid ways to register components.
21
     *
22
     *   * `register(Foo::class)` registers a component by it's class-name, and will try to
23
     *     automatically resolve all of it's constructor arguments.
24
     *
25
     *   * `register(Foo::class, ['bar'])` registers a component by it's class-name, and will
26
     *     use `'bar'` as the first constructor argument, and try to resolve the rest.
27
     *
28
     *   * `register(Foo::class, [$container->ref(Bar::class)])` creates a boxed reference to
29
     *     a registered component `Bar` and provides that as the first argument.
30
     *
31
     *   * `register(Foo::class, ['bat' => 'zap'])` registers a component by it's class-name
32
     *     and will use `'zap'` for the constructor argument named `$bat`, and try to resolve
33
     *     any other arguments.
34
     *
35
     *   * `register(Bar::class, Foo::class)` registers a component `Foo` under another name
36
     *     `Bar`, which might be an interface or an abstract class.
37
     *
38
     *   * `register(Bar::class, Foo::class, ['bar'])` same as above, but uses `'bar'` as the
39
     *     first argument.
40
     *
41
     *   * `register(Bar::class, Foo::class, ['bat' => 'zap'])` same as above, but, well, guess.
42
     *
43
     *   * `register(Bar::class, function (Foo $foo) { return new Bar(...); })` registers a
44
     *     component with a custom creation function.
45
     *
46
     *   * `register(Bar::class, function ($name) { ... }, [$container->ref('db.name')]);`
47
     *     registers a component creation function with a reference to a component "db.name"
48
     *     as the first argument.
49
     *
50
     * In effect, you can think of `$func` as being an optional argument.
51
     *
52
     * The provided parameter values may include any `BoxedValueInterface`, such as the boxed
53
     * component referenced created by {@see Container::ref()} - these will be unboxed as late
54
     * as possible.
55
     *
56
     * @param string                      $name                component name
57
     * @param callable|mixed|mixed[]|null $func_or_map_or_type creation function or class-name, or, if the first
58
     *                                                         argument is a class-name, a map of constructor arguments
59
     * @param mixed|mixed[]               $map                 mixed list/map of parameter values (and/or boxed values)
60
     *
61
     * @return void
62
     *
63
     * @throws ContainerException
64
     */
65 1
    public function register($name, $func_or_map_or_type = null, $map = [])
66
    {
67 1
        if (is_callable($func_or_map_or_type)) {
68
            // second argument is a creation function
69 1
            $func = $func_or_map_or_type;
70 1
        } elseif (is_string($func_or_map_or_type)) {
71
            // second argument is a class-name
72
            $func = function (Container $container) use ($func_or_map_or_type, $map) {
73 1
                return $container->create($func_or_map_or_type, $map);
74 1
            };
75 1
            $map = [];
76 1
        } elseif (is_array($func_or_map_or_type)) {
77
            // second argument is a map of constructor arguments
78
            $func = function (Container $container) use ($name, $func_or_map_or_type) {
79 1
                return $container->create($name, $func_or_map_or_type);
80 1
            };
81 1
        } elseif (is_null($func_or_map_or_type)) {
82
            // first argument is both the component and class-name
83
            $func = function (Container $container) use ($name) {
84 1
                return $container->create($name);
85 1
            };
86 1
        } else {
87 1
            throw new InvalidArgumentException("unexpected argument type for \$func_or_map_or_type: " . gettype($func_or_map_or_type));
88
        }
89
90 1
        $this->factory[$name] = $func;
91
92 1
        $this->factory_map[$name] = $map;
93
94 1
        unset($this->values[$name]);
95 1
    }
96
97
    /**
98
     * Directly inject a component into the container - use this to register components that
99
     * have already been created for some reason; for example, the Composer ClassLoader.
100
     *
101
     * @param string $name component name
102
     * @param mixed  $value
103
     *
104
     * @return void
105
     *
106
     * @throws ContainerException
107
     */
108 1
    public function set($name, $value)
109
    {
110 1
        $this->values[$name] = $value;
111
112 1
        unset($this->factory[$name], $this->factory_map[$name]);
113 1
    }
114
115
    /**
116
     * Register a component as an alias of another registered component.
117
     *
118
     * @param string $new_name new component name
119
     * @param string $ref_name referenced existing component name
120
     */
121 1
    public function alias($new_name, $ref_name)
122
    {
123
        $this->register($new_name, function (Container $container) use ($ref_name) {
124 1
            return $container->get($ref_name);
125 1
        });
126 1
    }
127
128
    /**
129
     * Register a configuration function, which will be applied as late as possible, e.g.
130
     * on first use of the component. For example:
131
     *
132
     *     $factory->configure('stack', function (MiddlewareStack $stack) {
133
     *         $stack->push(new MoreAwesomeMiddleware());
134
     *     });
135
     *
136
     * The given configuration function should include the configured component as the
137
     * first parameter to the closure, but may include any number of parameters, which
138
     * will be resolved and injected.
139
     *
140
     * The first argument (component name) is optional - that is, the name can be inferred
141
     * from a type-hint on the first parameter of the closure, so the following will work:
142
     *
143
     *     $factory->register(PageLayout::class);
144
     *
145
     *     $factory->configure(function (PageLayout $layout) {
146
     *         $layout->title = "Welcome";
147
     *     });
148
     *
149
     * In some cases, you may wish to fetch additional dependencies, by using additional
150
     * arguments, and specifying how these should be resolved, e.g. using
151
     * {@see Container::ref()} - for example:
152
     *
153
     *     $factory->register("cache", FileCache::class);
154
     *
155
     *     $factory->configure(
156
     *         "cache",
157
     *         function (FileCache $cache, $path) {
158
     *             $cache->setPath($path);
159
     *         },
160
     *         ['path' => $container->ref('cache.path')]
161
     *     );
162
     *
163
     * You can also use `configure()` to decorate objects, or manipulate (or replace) values:
164
     *
165
     *     $factory->configure('num_kittens', function ($num_kittens) {
166
     *         return $num_kittens + 6; // add another litter
167
     *     });
168
     *
169
     * In other words, if your closure returns something, the component will be replaced.
170
     *
171
     * @param string|callable        $name_or_func component name
172
     *                                             (or callable, if name is left out)
173
     * @param callable|mixed|mixed[] $func_or_map  `function (Type $component, ...) : void`
174
     *                                             (or parameter values, if name is left out)
175
     * @param mixed|mixed[]          $map          mixed list/map of parameter values and/or boxed values
176
     *                                             (or unused, if name is left out)
177
     *
178
     * @return void
179
     *
180
     * @throws ContainerException
181
     */
182 1
    public function configure($name_or_func, $func_or_map = null, $map = [])
183
    {
184 1
        if (is_callable($name_or_func)) {
185 1
            $func = $name_or_func;
186 1
            $map = $func_or_map ?: [];
187
188
            // no component name supplied, infer it from the closure:
189
190 1
            if ($func instanceof Closure) {
191 1
                $param = new ReflectionParameter($func, 0); // shortcut reflection for closures (as an optimization)
192 1
            } else {
193 1
                list($param) = Reflection::createFromCallable($func)->getParameters();
194
            }
195
196 1
            $name = Reflection::getParameterType($param); // infer component name from type-hint
197
198 1
            if ($name === null) {
199 1
                throw new InvalidArgumentException("no component-name or type-hint specified");
200
            }
201 1
        } else {
202 1
            $name = $name_or_func;
203 1
            $func = $func_or_map;
204
205 1
            if (!array_key_exists(0, $map)) {
206 1
                $map[0] = $this->ref($name);
207 1
            }
208
        }
209
210 1
        $this->config[$name][] = $func;
211 1
        $this->config_map[$name][] = $map;
212 1
    }
213
214
    /**
215
     * Creates a boxed reference to a component with a given name.
216
     *
217
     * You can use this in conjunction with `register()` to provide a component reference
218
     * without expanding that reference until first use - for example:
219
     *
220
     *     $factory->register(UserRepo::class, [$factory->ref('cache')]);
221
     *
222
     * This will reference the "cache" component and provide it as the first argument to the
223
     * constructor of `UserRepo` - compared with using `$container->get('cache')`, this has
224
     * the advantage of not actually activating the "cache" component until `UserRepo` is
225
     * used for the first time.
226
     *
227
     * Another reason (besides performance) to use references, is to defer the reference:
228
     *
229
     *     $factory->register(FileCache::class, ['root_path' => $factory->ref('cache.path')]);
230
     *
231
     * In this example, the component "cache.path" will be fetched from the container on
232
     * first use of `FileCache`, giving you a chance to configure "cache.path" later.
233
     *
234
     * @param string $name component name
235
     *
236
     * @return BoxedReference component reference
237
     */
238 1
    public function ref($name)
239
    {
240 1
        return new BoxedReference($name);
241
    }
242
243
    /**
244
     * Add a packaged configuration (a "provider") to this container.
245
     *
246
     * @see ProviderInterface
247
     *
248
     * @param ProviderInterface $provider
249
     *
250
     * @return void
251
     */
252 1
    public function add(ProviderInterface $provider)
253
    {
254 1
        $provider->register($this);
255 1
    }
256
257
    /**
258
     * Import all available components from a given `Container` instance.
259
     *
260
     * This does *not* copy the components from the given `Container`, but rather creates
261
     * registrations in *this* `Container` that `get()` components from another `Container`.
262
     *
263
     * This can be useful in scenarios where another `Container` instance has components
264
     * that survive several instances of a `Container` created by this `ContainerFactory` -
265
     * for example, this `ContainerFactory` might be used to define components that get
266
     * disposed after a single web-request, and the imported `Container` defines components
267
     * that can be safely reused across multiple web-requests.
268
     *
269
     * @param Container $container
270
     *
271
     * @return void
272
     */
273 1
    public function import(Container $container)
274
    {
275 1
        $names = array_merge(
276 1
            array_keys($container->factory),
0 ignored issues
show
Bug introduced by
The property factory cannot be accessed from this context as it is declared protected in class mindplay\unbox\Configuration.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
277 1
            array_keys($container->values)
0 ignored issues
show
Bug introduced by
The property values cannot be accessed from this context as it is declared protected in class mindplay\unbox\Configuration.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
278 1
        );
279
280 1
        foreach ($names as $name) {
281 1
            $this->register($name, function () use ($container, $name) {
282 1
                return $container->get($name);
283 1
            });
284 1
        }
285 1
    }
286
287
    /**
288
     * Create and bootstrap a new `Container` instance
289
     *
290
     * @return Container
291
     */
292 1
    public function createContainer()
293
    {
294 1
        return new Container($this);
295
    }
296
}
297