Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ public function __construct(...$middleware)
if ($middleware) {
$needsErrorHandlerNext = false;
foreach ($middleware as $handler) {
// load AccessLogHandler and ErrorHandler instance from last Container
if ($handler === AccessLogHandler::class || $handler === ErrorHandler::class) {
// load required internal classes from last Container
if (\in_array($handler, [AccessLogHandler::class, ErrorHandler::class, Container::class], true)) {
$handler = $container->getObject($handler);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public function __invoke(ServerRequestInterface $request, ?callable $next = null
*/
public function callable(string $class): callable
{
// may be any class name except AccessLogHandler or Container itself
\assert(!\in_array($class, [AccessLogHandler::class, self::class], true));

return function (ServerRequestInterface $request, ?callable $next = null) use ($class) {
try {
if ($this->container instanceof ContainerInterface) {
Expand Down Expand Up @@ -157,6 +160,10 @@ public function getObject(string $class) /*: object (PHP 7.2+) */
}
return $value;
} elseif ($this->container instanceof ContainerInterface) {
// fallback for missing required internal classes from PSR-11 adapter
if ($class === Container::class) {
return $this; // @phpstan-ignore-line returns instanceof `T`
}
return new $class();
}

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

return $this->container[$name];
} elseif ($name === self::class) {
// return container itself for self-references unless explicitly configured (see above)
return $this; // @phpstan-ignore-line returns instanceof `T`
}

// Check `$name` references a valid class name that can be autoloaded
Expand Down
5 changes: 5 additions & 0 deletions src/Io/RouteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
$last = \key($handlers);
$container = $this->container;
foreach ($handlers as $i => $handler) {
// unlikely: load container self-reference from container
if ($handler === Container::class) {
$handlers[$i] = $handler = $container->getObject($handler);
}

if ($handler instanceof Container && $i !== $last) {
$container = $handler;
unset($handlers[$i]);
Expand Down
5 changes: 3 additions & 2 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -642,14 +642,15 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa
$unused->expects($this->never())->method('getObject');

$container = $this->createMock(Container::class);
$container->expects($this->exactly(2))->method('getObject')->willReturnMap([
$container->expects($this->exactly(3))->method('getObject')->willReturnMap([
[AccessLogHandler::class, $accessLogHandler],
[ErrorHandler::class, $errorHandler],
[Container::class, $container],
]);

assert($unused instanceof Container);
assert($container instanceof Container);
$app = new App($unused, $container, $middleware, $unused);
$app = new App($unused, $container, Container::class, $middleware, $unused);

$ref = new ReflectionProperty($app, 'handler');
if (PHP_VERSION_ID < 80100) {
Expand Down
99 changes: 99 additions & 0 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,40 @@ public function __invoke(ServerRequestInterface $request): Response
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameViaAutowiringWithFactoryFunctionWithContainerDependency(): void
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new \stdClass()) {
/** @var \stdClass */
private $data;

public function __construct(\stdClass $data)
{
$this->data = $data;
}

public function __invoke(ServerRequestInterface $request): Response
{
return new Response(200, [], (string) json_encode($this->data));
}
};

$container = new Container([
\stdClass::class => function (Container $container) {
return (object)['container' => spl_object_hash($container)];
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$response = $callable($request);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"container":"' . spl_object_hash($container) . '"}', (string) $response->getBody());
}

public function testCallableTwiceReturnsCallableForClassNameViaAutowiringWithFactoryFunctionForDependencyWillCallFactoryOnlyOnce(): void
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down Expand Up @@ -2555,6 +2589,43 @@ public function testGetObjectReturnsAccessLogHandlerInstanceFromConfig(): void
$this->assertSame($accessLogHandler, $ret);
}

public function testGetObjectReturnsSelfContainerByDefault(): void
{
$container = new Container([]);

$ret = $container->getObject(Container::class);

$this->assertSame($container, $ret);
}

public function testGetObjectReturnsOtherContainerFromConfig(): void
{
$other = new Container();

$container = new Container([
Container::class => $other
]);

$ret = $container->getObject(Container::class);

$this->assertSame($other, $ret);
}

public function testGetObjectReturnsOtherContainerFromFactoryFunction(): void
{
$other = new Container();

$container = new Container([
Container::class => function () use ($other) {
return $other;
}
]);

$ret = $container->getObject(Container::class);

$this->assertSame($other, $ret);
}

public function testGetObjectReturnsAccessLogHandlerInstanceFromPsrContainer(): void
{
$accessLogHandler = new AccessLogHandler();
Expand Down Expand Up @@ -2585,6 +2656,20 @@ public function testGetObjectReturnsDefaultAccessLogHandlerInstanceIfPsrContaine
$this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler);
}

public function testGetObjectReturnsSelfContainerIfPsrContainerHasNoEntry(): void
{
$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->once())->method('has')->with(Container::class)->willReturn(false);
$psr->expects($this->never())->method('get');

assert($psr instanceof ContainerInterface);
$container = new Container($psr);

$ret = $container->getObject(Container::class);

$this->assertSame($container, $ret);
}

public function testGetObjectThrowsIfFactoryFunctionThrows(): void
{
$container = new Container([
Expand Down Expand Up @@ -2636,6 +2721,20 @@ public function testGetObjectThrowsIfFactoryFunctionIsRecursive(): void
$container->getObject(AccessLogHandler::class);
}

public function testGetObjectThrowsIfFactoryFunctionHasRecursiveContainerArgument(): void
{
$line = __LINE__ + 2;
$container = new Container([
Container::class => function (Container $container): Container {
return $container;
}
]);

$this->expectException(\Error::class);
$this->expectExceptionMessage('Argument #1 ($container) of {closure:' . __FILE__ . ':' . $line .'}() for FrameworkX\Container is recursive');
$container->getObject(Container::class);
}

public function testGetObjectThrowsIfConfigReferencesInterface(): void
{
$container = new Container([
Expand Down
39 changes: 38 additions & 1 deletion tests/Io/RouteHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ public function testMapRouteWithContainerAndControllerClassNameAddsRouteOnRouter
$handler->map(['GET'], '/', $container, \stdClass::class);
}

public function testMapRouteWithContainerClassNameAndControllerClassNameAddsRouteOnRouterWithControllerCallableFromOtherContainer(): void
{
$controller = function () { };

$other = $this->createMock(Container::class);
$other->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller);

$container = $this->createMock(Container::class);
$container->expects($this->once())->method('getObject')->with(Container::class)->willReturn($other);
assert($container instanceof Container);

$handler = new RouteHandler($container);

$router = $this->createMock(RouteCollector::class);
$router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller);

$ref = new \ReflectionProperty($handler, 'routeCollector');
if (PHP_VERSION_ID < 80100) {
$ref->setAccessible(true);
}
$ref->setValue($handler, $router);

$handler->map(['GET'], '/', Container::class, \stdClass::class);
}

public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatProxyRequestsAreNotAllowed(): void
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down Expand Up @@ -329,7 +354,7 @@ public function testHandleRequestWithOptionsAsteriskRequestReturnsResponseFromMa
$this->assertSame($response, $ret);
}

public function testHandleRequestWithContainerOnlyThrows(): void
public function testHandleRequestWithContainerInstanceOnlyThrows(): void
{
$request = new ServerRequest('GET', 'http://example.com/');

Expand All @@ -340,4 +365,16 @@ public function testHandleRequestWithContainerOnlyThrows(): void
$this->expectExceptionMessage('Container should not be used as final request handler');
$handler($request);
}

public function testHandleRequestWithContainerClassOnlyThrows(): void
{
$request = new ServerRequest('GET', 'http://example.com/');

$handler = new RouteHandler();
$handler->map(['GET'], '/', Container::class);

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Container should not be used as final request handler');
$handler($request);
}
}