Completed
Push — master ( 261083...452422 )
by Rasmus
02:26
created

ContainerFactory::createContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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
    public function alias($new_name, $ref_name)
122
    {
123 1
        $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
     * Create and bootstrap a new `Container` instance
259
     *
260
     * @return Container
261
     */
262 1
    public function createContainer()
263
    {
264 1
        return new Container($this);
265
    }
266
}
267