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

ContainerFactory::configure()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 17
cts 17
cp 1
rs 9.1768
c 0
b 0
f 0
cc 5
nc 6
nop 3
crap 5
1
<?php
2
3
namespace mindplay\unbox;
4
5
use Closure;
6
use Psr\Container\ContainerInterface;
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 InvalidArgumentException
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 1
    public function set($name, $value)
107
    {
108 1
        $this->values[$name] = $value;
109
110 1
        unset($this->factory[$name], $this->factory_map[$name]);
111 1
    }
112
113
    /**
114
     * Register a component as an alias of another registered component.
115
     *
116
     * @param string $new_name new component name
117
     * @param string $ref_name referenced existing component name
118
     */
119
    public function alias($new_name, $ref_name)
120
    {
121 1
        $this->register($new_name, function (Container $container) use ($ref_name) {
122 1
            return $container->get($ref_name);
123 1
        });
124 1
    }
125
126
    /**
127
     * Register a configuration function, which will be applied as late as possible, e.g.
128
     * on first use of the component. For example:
129
     *
130
     *     $factory->configure('stack', function (MiddlewareStack $stack) {
131
     *         $stack->push(new MoreAwesomeMiddleware());
132
     *     });
133
     *
134
     * The given configuration function should include the configured component as the
135
     * first parameter to the closure, but may include any number of parameters, which
136
     * will be resolved and injected.
137
     *
138
     * The first argument (component name) is optional - that is, the name can be inferred
139
     * from a type-hint on the first parameter of the closure, so the following will work:
140
     *
141
     *     $factory->register(PageLayout::class);
142
     *
143
     *     $factory->configure(function (PageLayout $layout) {
144
     *         $layout->title = "Welcome";
145
     *     });
146
     *
147
     * In some cases, you may wish to fetch additional dependencies, by using additional
148
     * arguments, and specifying how these should be resolved, e.g. using
149
     * {@see Container::ref()} - for example:
150
     *
151
     *     $factory->register("cache", FileCache::class);
152
     *
153
     *     $factory->configure(
154
     *         "cache",
155
     *         function (FileCache $cache, $path) {
156
     *             $cache->setPath($path);
157
     *         },
158
     *         ['path' => $container->ref('cache.path')]
159
     *     );
160
     *
161
     * You can also use `configure()` to decorate objects, or manipulate (or replace) values:
162
     *
163
     *     $factory->configure('num_kittens', function ($num_kittens) {
164
     *         return $num_kittens + 6; // add another litter
165
     *     });
166
     *
167
     * In other words, if your closure returns something, the component will be replaced.
168
     *
169
     * If you use multiple contexts, note that it *is* possible to configure a component from
170
     * a parent context, but this *can* have unintended side-effects - if you're going to
171
     * configure a component from a parent context, you shouldn't change it's state, but
172
     * instead `return` a new component instance (e.g. `clone` or decorator) or a new value.
173
     *
174
     * @param string|callable        $name_or_func component name
175
     *                                             (or callable, if name is left out)
176
     * @param callable|mixed|mixed[] $func_or_map  `function (Type $component, ...) : void`
177
     *                                             (or parameter values, if name is left out)
178
     * @param mixed|mixed[]          $map          mixed list/map of parameter values and/or boxed values
179
     *                                             (or unused, if name is left out)
180
     *
181
     * @return void
182
     *
183
     * @throws InvalidArgumentException
184
     */
185 1
    public function configure($name_or_func, $func_or_map = null, $map = [])
186
    {
187 1
        if (is_callable($name_or_func)) {
188 1
            $func = $name_or_func;
189 1
            $map = $func_or_map ?: [];
190
191
            // no component name supplied, infer it from the closure:
192
193 1
            $param = Reflection::getFirstParameter($func);
194
195 1
            $name = Reflection::getParameterType($param); // infer component name from type-hint
196
197 1
            if ($name === null) {
198 1
                throw new InvalidArgumentException("no component-name or type-hint specified");
199
            }
200 1
        } else {
201 1
            $name = $name_or_func;
202 1
            $func = $func_or_map;
203
204 1
            if (!array_key_exists(0, $map)) {
205 1
                $map[0] = $this->ref($name);
206 1
            }
207
        }
208
209 1
        $this->config[$name][] = $func;
210 1
        $this->config_map[$name][] = $map;
211 1
    }
212
213
    /**
214
     * Creates a boxed reference to a component with a given name.
215
     *
216
     * You can use this in conjunction with `register()` to provide a component reference
217
     * without expanding that reference until first use - for example:
218
     *
219
     *     $factory->register(UserRepo::class, [$factory->ref('cache')]);
220
     *
221
     * This will reference the "cache" component and provide it as the first argument to the
222
     * constructor of `UserRepo` - compared with using `$container->get('cache')`, this has
223
     * the advantage of not actually activating the "cache" component until `UserRepo` is
224
     * used for the first time.
225
     *
226
     * Another reason (besides performance) to use references, is to defer the reference:
227
     *
228
     *     $factory->register(FileCache::class, ['root_path' => $factory->ref('cache.path')]);
229
     *
230
     * In this example, the component "cache.path" will be fetched from the container on
231
     * first use of `FileCache`, giving you a chance to configure "cache.path" later.
232
     *
233
     * @param string $name component name
234
     *
235
     * @return BoxedReference component reference
236
     */
237 1
    public function ref($name)
238
    {
239 1
        return new BoxedReference($name);
240
    }
241
242
    /**
243
     * Add a packaged configuration (a "provider") to this container.
244
     *
245
     * @see ProviderInterface
246
     *
247
     * @param ProviderInterface $provider
248
     *
249
     * @return void
250
     */
251 1
    public function add(ProviderInterface $provider)
252
    {
253 1
        $provider->register($this);
254 1
    }
255
256
    /**
257
     * Register a `ContainerFactory` sub-context, which may then be bootstrapped
258
     * via the {@see configureContext()} method.
259
     *
260
     * @param string $name context-name
261
     *
262
     * @return void
263
     *
264
     * @see configureContext()
265
     */
266 1
    public function registerContext($name)
267
    {
268 1
        if (preg_match('/^[a-z][a-z0-9_]*$/i', $name) !== 1) {
269
            throw new ContainerException("invalid context name: {$name}");
270
        }
271
272 1
        $this->register("unbox.context.{$name}", get_class($this));
273 1
    }
274
275
    /**
276
     * Configure a `ContainerFactory` sub-context previously registered
277
     * via the {@see registerContext()} method.
278
     *
279
     * @param callable $func `function (ContainerFactory $context_name, ...) : void`
280
     *
281
     * @return void
282
     *
283
     * @see registerContext()
284
     */
285 1
    public function configureContext($func)
286
    {
287 1
        $param = Reflection::getFirstParameter($func);
288
289 1
        $name = $param->getName();
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
290
291 1
        $this->configure("unbox.context.{$name}", $func);
292 1
    }
293
294
    /**
295
     * Create and bootstrap the root `Container` instance
296
     *
297
     * @param ContainerInterface|null $parent optional parent Container (from a parent context, if any)
298
     *
299
     * @return Container
300
     */
301 1
    public function createContainer(ContainerInterface $parent = null)
302
    {
303
        // TODO drop support for legacy PSR-11 interface to avoid type-hinting problem here
304
        // TODO can we internalize the `$parent` argument, or should we leave it public in case it's useful to someone?
305 1
        return new Container($this, $parent);
0 ignored issues
show
Bug introduced by
It seems like $parent defined by parameter $parent on line 301 can also be of type object<Psr\Container\ContainerInterface>; however, mindplay\unbox\Container::__construct() does only seem to accept null|object<Interop\Container\ContainerInterface>, 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...
306
    }
307
}
308