Skip to content

Commit 053ab66

Browse files
authored
Merge pull request #178 from clue-labs/container-strings
Support string variables in container configuration for factory functions
2 parents 440cf7e + 350cd67 commit 053ab66

3 files changed

Lines changed: 450 additions & 25 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ the dependency injection container like this:
209209
require __DIR__ . '/../vendor/autoload.php';
210210

211211
$container = new FrameworkX\Container([
212-
Acme\Todo\HelloController::class => fn() => new Acme\Todo\HelloController();
212+
Acme\Todo\HelloController::class => fn() => new Acme\Todo\HelloController()
213213
]);
214214

215215

@@ -284,6 +284,37 @@ $container = new FrameworkX\Container([
284284
// …
285285
```
286286

287+
Factory functions used in the container configuration map may also reference
288+
string variables defined in the container configuration. You may also use
289+
factory functions that return string variables. This can be particularly useful
290+
when combining autowiring with some manual configuration like this:
291+
292+
```php title="public/index.php"
293+
<?php
294+
295+
require __DIR__ . '/../vendor/autoload.php';
296+
297+
$container = new FrameworkX\Container([
298+
Acme\Todo\UserController::class => function (string $name, string $hostname) {
299+
// example UserController class requires two string arguments
300+
return new Acme\Todo\UserController($name, $hostname);
301+
},
302+
'name' => 'Acme',
303+
'hostname' => fn(): string => gethostname()
304+
]);
305+
306+
// …
307+
```
308+
309+
> ℹ️ **Avoiding name conflicts**
310+
>
311+
> Note that class names and string variables share the same container
312+
> configuration map and as such might be subject to name collisions as a single
313+
> entry may only have a single value. For this reason, container variables will
314+
> only be used for container functions by default. We highly recommend using
315+
> namespaced class names like in the previous example. You may also want to make
316+
> sure that container variables use unique names prefixed with your vendor name.
317+
287318
The container configuration may also be used to map a class name to a different
288319
class name that implements the same interface, either by mapping between two
289320
class names or using a factory function that returns a class name. This is

src/Container.php

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
*/
1111
class Container
1212
{
13-
/** @var array<class-string,object|callable():(object|class-string)>|ContainerInterface */
13+
/** @var array<string,object|callable():(object|string)|string>|ContainerInterface */
1414
private $container;
1515

16-
/** @var array<class-string,callable():(object|class-string) | object | class-string>|ContainerInterface $loader */
16+
/** @var array<string,callable():(object|string) | object | string>|ContainerInterface $loader */
1717
public function __construct($loader = [])
1818
{
1919
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
@@ -23,11 +23,7 @@ public function __construct($loader = [])
2323
}
2424

2525
foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
26-
if (\is_string($value)) {
27-
$loader[$name] = static function () use ($value) {
28-
return $value;
29-
};
30-
} elseif (!$value instanceof \Closure && !$value instanceof $name) {
26+
if (!\is_string($value) && !$value instanceof \Closure && !$value instanceof $name) {
3127
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
3228
}
3329
}
@@ -67,7 +63,7 @@ public function callable(string $class): callable
6763
if ($this->container instanceof ContainerInterface) {
6864
$handler = $this->container->get($class);
6965
} else {
70-
$handler = $this->load($class);
66+
$handler = $this->loadObject($class);
7167
}
7268
} catch (\Throwable $e) {
7369
throw new \BadMethodCallException(
@@ -102,7 +98,7 @@ public function getAccessLogHandler(): AccessLogHandler
10298
return new AccessLogHandler();
10399
}
104100
}
105-
return $this->load(AccessLogHandler::class);
101+
return $this->loadObject(AccessLogHandler::class);
106102
}
107103

108104
/** @internal */
@@ -115,21 +111,33 @@ public function getErrorHandler(): ErrorHandler
115111
return new ErrorHandler();
116112
}
117113
}
118-
return $this->load(ErrorHandler::class);
114+
return $this->loadObject(ErrorHandler::class);
119115
}
120116

121117
/**
122-
* @param class-string $name
123-
* @return object
124-
* @throws \BadMethodCallException
118+
* @template T
119+
* @param class-string<T> $name
120+
* @return T
121+
* @throws \BadMethodCallException if object of type $name can not be loaded
125122
*/
126-
private function load(string $name, int $depth = 64)
123+
private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */
127124
{
128125
if (isset($this->container[$name])) {
129-
if ($this->container[$name] instanceof \Closure) {
126+
if (\is_string($this->container[$name])) {
127+
if ($depth < 1) {
128+
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
129+
}
130+
131+
$value = $this->loadObject($this->container[$name], $depth - 1);
132+
if (!$value instanceof $name) {
133+
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
134+
}
135+
136+
$this->container[$name] = $value;
137+
} elseif ($this->container[$name] instanceof \Closure) {
130138
// build list of factory parameters based on parameter types
131139
$closure = new \ReflectionFunction($this->container[$name]);
132-
$params = $this->loadFunctionParams($closure, $depth);
140+
$params = $this->loadFunctionParams($closure, $depth, true);
133141

134142
// invoke factory with list of parameters
135143
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);
@@ -139,14 +147,17 @@ private function load(string $name, int $depth = 64)
139147
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
140148
}
141149

142-
$value = $this->load($value, $depth - 1);
143-
} elseif (!$value instanceof $name) {
150+
$value = $this->loadObject($value, $depth - 1);
151+
}
152+
if (!$value instanceof $name) {
144153
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
145154
}
146155

147156
$this->container[$name] = $value;
148157
}
149158

159+
assert($this->container[$name] instanceof $name);
160+
150161
return $this->container[$name];
151162
}
152163

@@ -170,13 +181,14 @@ private function load(string $name, int $depth = 64)
170181

171182
// build list of constructor parameters based on parameter types
172183
$ctor = $class->getConstructor();
173-
$params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth);
184+
$params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false);
174185

175186
// instantiate with list of parameters
176187
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
177188
}
178189

179-
private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth): array
190+
/** @throws \BadMethodCallException if either parameter can not be loaded */
191+
private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array
180192
{
181193
$params = [];
182194
foreach ($function->getParameters() as $parameter) {
@@ -206,6 +218,14 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
206218
}
207219

208220
assert($type instanceof \ReflectionNamedType);
221+
222+
// load string variables from container
223+
if ($allowVariables && $type->getName() === 'string') {
224+
$params[] = $this->loadVariable($parameter->getName(), $depth);
225+
continue;
226+
}
227+
228+
// abort for other primitive types
209229
if ($type->isBuiltin()) {
210230
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
211231
}
@@ -215,12 +235,47 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
215235
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
216236
}
217237

218-
$params[] = $this->load($type->getName(), $depth - 1);
238+
$params[] = $this->loadObject($type->getName(), $depth - 1);
219239
}
220240

221241
return $params;
222242
}
223243

244+
/** @throws \BadMethodCallException if $name is not a valid string variable */
245+
private function loadVariable(string $name, int $depth): string
246+
{
247+
if (!isset($this->container[$name])) {
248+
throw new \BadMethodCallException('Container variable $' . $name . ' is not defined');
249+
}
250+
251+
if ($this->container[$name] instanceof \Closure) {
252+
if ($depth < 1) {
253+
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
254+
}
255+
256+
// build list of factory parameters based on parameter types
257+
$closure = new \ReflectionFunction($this->container[$name]);
258+
$params = $this->loadFunctionParams($closure, $depth - 1, true);
259+
260+
// invoke factory with list of parameters
261+
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);
262+
263+
if (!\is_string($value)) {
264+
throw new \BadMethodCallException('Container variable $' . $name . ' expected type string from factory, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
265+
}
266+
267+
$this->container[$name] = $value;
268+
}
269+
270+
$value = $this->container[$name];
271+
if (!\is_string($value)) {
272+
throw new \BadMethodCallException('Container variable $' . $name . ' expected type string, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
273+
}
274+
275+
return $value;
276+
}
277+
278+
/** @throws void */
224279
private static function parameterError(\ReflectionParameter $parameter): string
225280
{
226281
$name = $parameter->getDeclaringFunction()->getShortName();

0 commit comments

Comments
 (0)