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