Completed
Pull Request — master (#7)
by Rasmus
04:28
created

ContainerFactory::alias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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