Skip to content

Commit b6a8230

Browse files
committed
Support container variables from factory function that returns string
1 parent 9b1d235 commit b6a8230

3 files changed

Lines changed: 169 additions & 14 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,21 +285,22 @@ $container = new FrameworkX\Container([
285285
```
286286

287287
Factory functions used in the container configuration map may also reference
288-
string variables defined in the container configuration. This can be
289-
particularly useful when combining autowiring with some manual configuration
290-
like this:
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:
291291

292292
```php title="public/index.php"
293293
<?php
294294

295295
require __DIR__ . '/../vendor/autoload.php';
296296

297297
$container = new FrameworkX\Container([
298-
Acme\Todo\UserController::class => function (string $name) {
299-
// example UserController class requires single string argument
300-
return new Acme\Todo\UserController($name);
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);
301301
},
302-
'name' => 'Acme'
302+
'name' => 'Acme',
303+
'hostname' => fn (): string => gethostname()
303304
]);
304305

305306
// …

src/Container.php

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

16-
/** @var array<string,callable():(object|class-string) | object | 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) {
@@ -221,7 +221,7 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
221221

222222
// load string variables from container
223223
if ($allowVariables && $type->getName() === 'string') {
224-
$params[] = $this->loadVariable($parameter->getName());
224+
$params[] = $this->loadVariable($parameter->getName(), $depth);
225225
continue;
226226
}
227227

@@ -242,12 +242,31 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
242242
}
243243

244244
/** @throws \BadMethodCallException if $name is not a valid string variable */
245-
private function loadVariable(string $name): string
245+
private function loadVariable(string $name, int $depth): string
246246
{
247247
if (!isset($this->container[$name])) {
248248
throw new \BadMethodCallException('Container variable $' . $name . ' is not defined');
249249
}
250250

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+
251270
$value = $this->container[$name];
252271
if (!\is_string($value)) {
253272
throw new \BadMethodCallException('Container variable $' . $name . ' expected type string, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));

tests/ContainerTest.php

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,77 @@ public function __invoke(ServerRequestInterface $request)
274274
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
275275
}
276276

277+
public function testCallableReturnsCallableForClassNameMappedFromFactoryWithStringVariableMappedFromFactory()
278+
{
279+
$request = new ServerRequest('GET', 'http://example.com/');
280+
281+
$controller = new class(new \stdClass()) {
282+
private $data;
283+
284+
public function __construct(\stdClass $data)
285+
{
286+
$this->data = $data;
287+
}
288+
289+
public function __invoke(ServerRequestInterface $request)
290+
{
291+
return new Response(200, [], json_encode($this->data));
292+
}
293+
};
294+
295+
$container = new Container([
296+
\stdClass::class => function (string $username) {
297+
return (object) ['name' => $username];
298+
},
299+
'username' => function () {
300+
return 'Alice';
301+
}
302+
]);
303+
304+
$callable = $container->callable(get_class($controller));
305+
306+
$response = $callable($request);
307+
$this->assertInstanceOf(ResponseInterface::class, $response);
308+
$this->assertEquals(200, $response->getStatusCode());
309+
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
310+
}
311+
312+
public function testCallableReturnsCallableForClassNameReferencingVariableMappedFromFactoryReferencingVariable()
313+
{
314+
$request = new ServerRequest('GET', 'http://example.com/');
315+
316+
$controller = new class(new \stdClass()) {
317+
private $data;
318+
319+
public function __construct(\stdClass $data)
320+
{
321+
$this->data = $data;
322+
}
323+
324+
public function __invoke(ServerRequestInterface $request)
325+
{
326+
return new Response(200, [], json_encode($this->data));
327+
}
328+
};
329+
330+
$container = new Container([
331+
\stdClass::class => function (string $username) {
332+
return (object) ['name' => $username];
333+
},
334+
'username' => function (string $role) {
335+
return strtoupper($role);
336+
},
337+
'role' => 'admin'
338+
]);
339+
340+
$callable = $container->callable(get_class($controller));
341+
342+
$response = $callable($request);
343+
$this->assertInstanceOf(ResponseInterface::class, $response);
344+
$this->assertEquals(200, $response->getStatusCode());
345+
$this->assertEquals('{"name":"ADMIN"}', (string) $response->getBody());
346+
}
347+
277348
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesUnknownVariable()
278349
{
279350
$request = new ServerRequest('GET', 'http://example.com/');
@@ -305,7 +376,71 @@ public function __invoke(ServerRequestInterface $request)
305376
$callable($request);
306377
}
307378

308-
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableOfUnexpectedType()
379+
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesRecursiveVariable()
380+
{
381+
$request = new ServerRequest('GET', 'http://example.com/');
382+
383+
$controller = new class(new \stdClass()) {
384+
private $data;
385+
386+
public function __construct(\stdClass $data)
387+
{
388+
$this->data = $data;
389+
}
390+
391+
public function __invoke(ServerRequestInterface $request)
392+
{
393+
return new Response(200, [], json_encode($this->data));
394+
}
395+
};
396+
397+
$container = new Container([
398+
\stdClass::class => function (string $stdClass) {
399+
return (object) ['name' => $stdClass];
400+
}
401+
]);
402+
403+
$callable = $container->callable(get_class($controller));
404+
405+
$this->expectException(\BadMethodCallException::class);
406+
$this->expectExceptionMessage('Container variable $stdClass is recursive');
407+
$callable($request);
408+
}
409+
410+
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableMappedWithUnexpectedType()
411+
{
412+
$request = new ServerRequest('GET', 'http://example.com/');
413+
414+
$controller = new class('') {
415+
private $data;
416+
417+
public function __construct(string $stdClass)
418+
{
419+
$this->data = $stdClass;
420+
}
421+
422+
public function __invoke(ServerRequestInterface $request)
423+
{
424+
return new Response(200, [], json_encode($this->data));
425+
}
426+
};
427+
428+
$container = new Container([
429+
get_class($controller) => function (string $stdClass) use ($controller) {
430+
$class = get_class($controller);
431+
return new $class($stdClass);
432+
},
433+
\stdClass::class => (object) []
434+
]);
435+
436+
$callable = $container->callable(get_class($controller));
437+
438+
$this->expectException(\BadMethodCallException::class);
439+
$this->expectExceptionMessage('Container variable $stdClass expected type string, but got stdClass');
440+
$callable($request);
441+
}
442+
443+
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableMappedFromFactoryWithUnexpectedReturnType()
309444
{
310445
$request = new ServerRequest('GET', 'http://example.com/');
311446

@@ -328,14 +463,14 @@ public function __invoke(ServerRequestInterface $request)
328463
return (object) ['name' => $http];
329464
},
330465
'http' => function () {
331-
return 'http/3';
466+
return 3;
332467
}
333468
]);
334469

335470
$callable = $container->callable(get_class($controller));
336471

337472
$this->expectException(\BadMethodCallException::class);
338-
$this->expectExceptionMessage('Container variable $http expected type string, but got Closure');
473+
$this->expectExceptionMessage('Container variable $http expected type string from factory, but got integer');
339474
$callable($request);
340475
}
341476

0 commit comments

Comments
 (0)