Completed
Pull Request — master (#18)
by Rasmus
02:07
created

ContainerFactory::requires()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $messages of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
331 1
            throw new ContainerException(implode("\n", $messages));
332
        }
333
334 1
        return new Container($this);
335
    }
336
}
337