Skip to content

Commit e329ca1

Browse files
committed
Support loading Container self-reference from Container
1 parent 620de5a commit e329ca1

File tree

6 files changed

+157
-5
lines changed

6 files changed

+157
-5
lines changed

src/App.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public function __construct(...$middleware)
5252
if ($middleware) {
5353
$needsErrorHandlerNext = false;
5454
foreach ($middleware as $handler) {
55-
// load AccessLogHandler and ErrorHandler instance from last Container
56-
if ($handler === AccessLogHandler::class || $handler === ErrorHandler::class) {
55+
// load required internal classes from last Container
56+
if (\in_array($handler, [AccessLogHandler::class, ErrorHandler::class, Container::class], true)) {
5757
$handler = $container->getObject($handler);
5858
}
5959

src/Container.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public function __invoke(ServerRequestInterface $request, ?callable $next = null
7676
*/
7777
public function callable(string $class): callable
7878
{
79+
// may be any class name expect AccessLogHandler or Container itself
80+
\assert(!\in_array($class, [AccessLogHandler::class, self::class], true));
81+
7982
return function (ServerRequestInterface $request, ?callable $next = null) use ($class) {
8083
try {
8184
if ($this->container instanceof ContainerInterface) {
@@ -157,6 +160,10 @@ public function getObject(string $class) /*: object (PHP 7.2+) */
157160
}
158161
return $value;
159162
} elseif ($this->container instanceof ContainerInterface) {
163+
// fallback for missing required internal classes from PSR-11 adapter
164+
if ($class === Container::class) {
165+
return $this;
166+
}
160167
return new $class();
161168
}
162169

@@ -223,6 +230,9 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+)
223230
\assert($this->container[$name] instanceof $name);
224231

225232
return $this->container[$name];
233+
} elseif ($name === self::class) {
234+
// return container itself for self-references unless explicitly configured (see above)
235+
return $this;
226236
}
227237

228238
// Check `$name` references a valid class name that can be autoloaded

src/Io/RouteHandler.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
5555
$last = \key($handlers);
5656
$container = $this->container;
5757
foreach ($handlers as $i => $handler) {
58+
// unlikely: load container self-reference from container
59+
if ($handler === Container::class) {
60+
$handlers[$i] = $handler = $container->getObject($handler);
61+
}
62+
5863
if ($handler instanceof Container && $i !== $last) {
5964
$container = $handler;
6065
unset($handlers[$i]);

tests/AppTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,14 +642,15 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa
642642
$unused->expects($this->never())->method('getObject');
643643

644644
$container = $this->createMock(Container::class);
645-
$container->expects($this->exactly(2))->method('getObject')->willReturnMap([
645+
$container->expects($this->exactly(3))->method('getObject')->willReturnMap([
646646
[AccessLogHandler::class, $accessLogHandler],
647647
[ErrorHandler::class, $errorHandler],
648+
[Container::class, $container],
648649
]);
649650

650651
assert($unused instanceof Container);
651652
assert($container instanceof Container);
652-
$app = new App($unused, $container, $middleware, $unused);
653+
$app = new App($unused, $container, Container::class, $middleware, $unused);
653654

654655
$ref = new ReflectionProperty($app, 'handler');
655656
if (PHP_VERSION_ID < 80100) {

tests/ContainerTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,40 @@ public function __invoke(ServerRequestInterface $request): Response
289289
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
290290
}
291291

292+
public function testCallableReturnsCallableForClassNameViaAutowiringWithFactoryFunctionWithContainerDependency(): void
293+
{
294+
$request = new ServerRequest('GET', 'http://example.com/');
295+
296+
$controller = new class(new \stdClass()) {
297+
/** @var \stdClass */
298+
private $data;
299+
300+
public function __construct(\stdClass $data)
301+
{
302+
$this->data = $data;
303+
}
304+
305+
public function __invoke(ServerRequestInterface $request): Response
306+
{
307+
return new Response(200, [], (string) json_encode($this->data));
308+
}
309+
};
310+
311+
$container = new Container([
312+
\stdClass::class => function (Container $container) {
313+
return (object)['container' => spl_object_hash($container)];
314+
}
315+
]);
316+
317+
$callable = $container->callable(get_class($controller));
318+
$this->assertInstanceOf(\Closure::class, $callable);
319+
320+
$response = $callable($request);
321+
$this->assertInstanceOf(ResponseInterface::class, $response);
322+
$this->assertEquals(200, $response->getStatusCode());
323+
$this->assertEquals('{"container":"' . spl_object_hash($container) . '"}', (string) $response->getBody());
324+
}
325+
292326
public function testCallableTwiceReturnsCallableForClassNameViaAutowiringWithFactoryFunctionForDependencyWillCallFactoryOnlyOnce(): void
293327
{
294328
$request = new ServerRequest('GET', 'http://example.com/');
@@ -2555,6 +2589,43 @@ public function testGetObjectReturnsAccessLogHandlerInstanceFromConfig(): void
25552589
$this->assertSame($accessLogHandler, $ret);
25562590
}
25572591

2592+
public function testGetObjectReturnsSelfContainerByDefault(): void
2593+
{
2594+
$container = new Container([]);
2595+
2596+
$ret = $container->getObject(Container::class);
2597+
2598+
$this->assertSame($container, $ret);
2599+
}
2600+
2601+
public function testGetObjectReturnsOtherContainerFromConfig(): void
2602+
{
2603+
$other = new Container();
2604+
2605+
$container = new Container([
2606+
Container::class => $other
2607+
]);
2608+
2609+
$ret = $container->getObject(Container::class);
2610+
2611+
$this->assertSame($other, $ret);
2612+
}
2613+
2614+
public function testGetObjectReturnsOtherContainerFromFactoryFunction(): void
2615+
{
2616+
$other = new Container();
2617+
2618+
$container = new Container([
2619+
Container::class => function () use ($other) {
2620+
return $other;
2621+
}
2622+
]);
2623+
2624+
$ret = $container->getObject(Container::class);
2625+
2626+
$this->assertSame($other, $ret);
2627+
}
2628+
25582629
public function testGetObjectReturnsAccessLogHandlerInstanceFromPsrContainer(): void
25592630
{
25602631
$accessLogHandler = new AccessLogHandler();
@@ -2585,6 +2656,20 @@ public function testGetObjectReturnsDefaultAccessLogHandlerInstanceIfPsrContaine
25852656
$this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler);
25862657
}
25872658

2659+
public function testGetObjectReturnsSelfContainerIfPsrContainerHasNoEntry(): void
2660+
{
2661+
$psr = $this->createMock(ContainerInterface::class);
2662+
$psr->expects($this->once())->method('has')->with(Container::class)->willReturn(false);
2663+
$psr->expects($this->never())->method('get');
2664+
2665+
assert($psr instanceof ContainerInterface);
2666+
$container = new Container($psr);
2667+
2668+
$ret = $container->getObject(Container::class);
2669+
2670+
$this->assertSame($container, $ret);
2671+
}
2672+
25882673
public function testGetObjectThrowsIfFactoryFunctionThrows(): void
25892674
{
25902675
$container = new Container([
@@ -2636,6 +2721,20 @@ public function testGetObjectThrowsIfFactoryFunctionIsRecursive(): void
26362721
$container->getObject(AccessLogHandler::class);
26372722
}
26382723

2724+
public function testGetObjectThrowsIfFactoryFunctionHasRecursiveContainerArgument(): void
2725+
{
2726+
$line = __LINE__ + 2;
2727+
$container = new Container([
2728+
Container::class => function (Container $container): Container {
2729+
return $container;
2730+
}
2731+
]);
2732+
2733+
$this->expectException(\Error::class);
2734+
$this->expectExceptionMessage('Argument #1 ($container) of {closure:' . __FILE__ . ':' . $line .'}() for FrameworkX\Container is recursive');
2735+
$container->getObject(Container::class);
2736+
}
2737+
26392738
public function testGetObjectThrowsIfConfigReferencesInterface(): void
26402739
{
26412740
$container = new Container([

tests/Io/RouteHandlerTest.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,31 @@ public function testMapRouteWithContainerAndControllerClassNameAddsRouteOnRouter
113113
$handler->map(['GET'], '/', $container, \stdClass::class);
114114
}
115115

116+
public function testMapRouteWithContainerClassNameAndControllerClassNameAddsRouteOnRouterWithControllerCallableFromOtherContainer(): void
117+
{
118+
$controller = function () { };
119+
120+
$other = $this->createMock(Container::class);
121+
$other->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller);
122+
123+
$container = $this->createMock(Container::class);
124+
$container->expects($this->once())->method('getObject')->with(Container::class)->willReturn($other);
125+
assert($container instanceof Container);
126+
127+
$handler = new RouteHandler($container);
128+
129+
$router = $this->createMock(RouteCollector::class);
130+
$router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller);
131+
132+
$ref = new \ReflectionProperty($handler, 'routeCollector');
133+
if (PHP_VERSION_ID < 80100) {
134+
$ref->setAccessible(true);
135+
}
136+
$ref->setValue($handler, $router);
137+
138+
$handler->map(['GET'], '/', Container::class, \stdClass::class);
139+
}
140+
116141
public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatProxyRequestsAreNotAllowed(): void
117142
{
118143
$request = new ServerRequest('GET', 'http://example.com/');
@@ -329,7 +354,7 @@ public function testHandleRequestWithOptionsAsteriskRequestReturnsResponseFromMa
329354
$this->assertSame($response, $ret);
330355
}
331356

332-
public function testHandleRequestWithContainerOnlyThrows(): void
357+
public function testHandleRequestWithContainerInstanceOnlyThrows(): void
333358
{
334359
$request = new ServerRequest('GET', 'http://example.com/');
335360

@@ -340,4 +365,16 @@ public function testHandleRequestWithContainerOnlyThrows(): void
340365
$this->expectExceptionMessage('Container should not be used as final request handler');
341366
$handler($request);
342367
}
368+
369+
public function testHandleRequestWithContainerClassOnlyThrows(): void
370+
{
371+
$request = new ServerRequest('GET', 'http://example.com/');
372+
373+
$handler = new RouteHandler();
374+
$handler->map(['GET'], '/', Container::class);
375+
376+
$this->expectException(\BadMethodCallException::class);
377+
$this->expectExceptionMessage('Container should not be used as final request handler');
378+
$handler($request);
379+
}
343380
}

0 commit comments

Comments
 (0)