diff --git a/src/Container.php b/src/Container.php index 3ea15fd..111fe63 100644 --- a/src/Container.php +++ b/src/Container.php @@ -16,31 +16,42 @@ class Container /** @var bool */ private $useProcessEnv; - /** @param array|ContainerInterface $loader */ - public function __construct($loader = []) + /** + * @param array|ContainerInterface $config + * @throws \TypeError if given $config is invalid + */ + public function __construct($config = []) { - /** @var mixed $loader explicit type check for mixed if user ignores parameter type */ - if (!\is_array($loader) && !$loader instanceof ContainerInterface) { + /** @var mixed $config explicit type check for mixed if user ignores parameter type */ + if (!\is_array($config) && !$config instanceof ContainerInterface) { throw new \TypeError( - 'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . $this->gettype($loader) . ' given' + 'Argument #1 ($config) must be of type array|Psr\Container\ContainerInterface, ' . $this->gettype($config) . ' given' ); } - foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) { - if ( - (!\is_object($value) && !\is_scalar($value) && $value !== null) || - (!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false) - ) { - throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . $this->gettype($value)); + foreach (($config instanceof ContainerInterface ? [] : $config) as $name => $value) { + if (!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false) { + throw new \TypeError( + 'Argument #1 ($config) for key "' . $name . '" must be of type ' . $name . '|Closure|string, ' . $this->gettype($value) . ' given' + ); + } + if (!\is_object($value) && !\is_scalar($value) && $value !== null) { + throw new \TypeError( + 'Argument #1 ($config) for key "' . $name . '" must be of type object|string|int|float|bool|null|Closure, ' . $this->gettype($value) . ' given' + ); } } - $this->container = $loader; + $this->container = $config; // prefer reading environment from `$_ENV` and `$_SERVER`, only fall back to `getenv()` in thread-safe environments $this->useProcessEnv = \ZEND_THREAD_SAFE === false || \in_array(\PHP_SAPI, ['cli', 'cli-server', 'cgi-fcgi', 'fpm-fcgi'], true); } - /** @return mixed */ + /** + * @return mixed returns whatever the $next handler returns + * @throws \BadMethodCallException if used as a final request handler + * @throws \Throwable if $next handler throws unexpected exception + */ public function __invoke(ServerRequestInterface $request, ?callable $next = null) { if ($next === null) { @@ -60,16 +71,12 @@ public function __invoke(ServerRequestInterface $request, ?callable $next = null /** * @param class-string $class * @return callable(ServerRequestInterface,?callable=null) + * @throws void * @internal */ public function callable(string $class): callable { return function (ServerRequestInterface $request, ?callable $next = null) use ($class) { - // Check `$class` references a valid class name that can be autoloaded - if (\is_array($this->container) && !\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) { - throw new \BadMethodCallException('Request handler class ' . $class . ' not found'); - } - try { if ($this->container instanceof ContainerInterface) { $handler = $this->container->get($class); @@ -77,7 +84,7 @@ public function callable(string $class): callable $handler = $this->loadObject($class); } } catch (\Throwable $e) { - throw new \BadMethodCallException( + throw new \Error( 'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(), 0, $e @@ -88,7 +95,9 @@ public function callable(string $class): callable // This initial version is intentionally limited to checking the method name only. // A follow-up version will likely use reflection to check request handler argument types. if (!is_callable($handler)) { - throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method'); + throw new \Error( + 'Request handler ' . \explode("\0", $class)[0] . ' has no public __invoke() method' + ); } // invoke request handler as middleware handler or final controller @@ -99,7 +108,11 @@ public function callable(string $class): callable }; } - /** @internal */ + /** + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Throwable if container factory function throws unexpected exception + * @internal + */ public function getEnv(string $name): ?string { assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1); @@ -107,19 +120,25 @@ public function getEnv(string $name): ?string if ($this->container instanceof ContainerInterface && $this->container->has($name)) { $value = $this->container->get($name); } elseif ($this->hasVariable($name)) { - $value = $this->loadVariable($name, 'mixed', true, 64); + $value = $this->loadVariable($name); } else { return null; } if (!\is_string($value) && $value !== null) { - throw new \TypeError('Environment variable $' . $name . ' expected type string|null, but got ' . $this->gettype($value)); + throw new \TypeError( + 'Return value of ' . __METHOD__ . '() for $' . $name . ' must be of type string|null, ' . $this->gettype($value) . ' returned' + ); } return $value; } - /** @internal */ + /** + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Throwable if container factory function throws unexpected exception + * @internal + */ public function getAccessLogHandler(): AccessLogHandler { if ($this->container instanceof ContainerInterface) { @@ -133,7 +152,11 @@ public function getAccessLogHandler(): AccessLogHandler return $this->loadObject(AccessLogHandler::class); } - /** @internal */ + /** + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Throwable if container factory function throws unexpected exception + * @internal + */ public function getErrorHandler(): ErrorHandler { if ($this->container instanceof ContainerInterface) { @@ -151,7 +174,9 @@ public function getErrorHandler(): ErrorHandler * @template T of object * @param class-string $name * @return T - * @throws \BadMethodCallException if object of type $name can not be loaded + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Error if object of type $name can not be loaded + * @throws \Throwable if container factory function throws unexpected exception */ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */ { @@ -160,39 +185,45 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) if (\array_key_exists($name, $this->container)) { if (\is_string($this->container[$name])) { if ($depth < 1) { - throw new \BadMethodCallException('Factory for ' . $name . ' is recursive'); + throw new \Error('Container config for ' . $name . ' is recursive'); } // @phpstan-ignore-next-line because type of container value is explicitly checked after getting here $value = $this->loadObject($this->container[$name], $depth - 1); if (!$value instanceof $name) { - throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . $this->gettype($value)); + throw new \TypeError( + 'Return value of ' . __METHOD__ . '() for ' . $name . ' must be of type ' . $name . ', ' . $this->gettype($value) . ' returned' + ); } $this->container[$name] = $value; } elseif ($this->container[$name] instanceof \Closure) { // build list of factory parameters based on parameter types $closure = new \ReflectionFunction($this->container[$name]); - $params = $this->loadFunctionParams($closure, $depth, true); + $params = $this->loadFunctionParams($closure, $depth, true, \explode("\0", $name)[0]); // invoke factory with list of parameters $value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params); if (\is_string($value)) { if ($depth < 1) { - throw new \BadMethodCallException('Factory for ' . $name . ' is recursive'); + throw new \Error('Container config for ' . $name . ' is recursive'); } // @phpstan-ignore-next-line because type of container value is explicitly checked after getting here $value = $this->loadObject($value, $depth - 1); } if (!$value instanceof $name) { - throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . $this->gettype($value)); + throw new \TypeError( + 'Return value of ' . self::functionName($closure) . ' for ' . $name . ' must be of type ' . $name . ', ' . $this->gettype($value) . ' returned' + ); } $this->container[$name] = $value; } elseif (!$this->container[$name] instanceof $name) { - throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . $this->gettype($this->container[$name])); + throw new \TypeError( + 'Return value of ' . __METHOD__ . '() for ' . $name . ' must be of type ' . $name . ', ' . $this->gettype($this->container[$name]) . ' returned' + ); } assert($this->container[$name] instanceof $name); @@ -202,7 +233,7 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) // Check `$name` references a valid class name that can be autoloaded if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) { - throw new \BadMethodCallException('Class ' . $name . ' not found'); + throw new \Error('Class ' . $name . ' not found'); } $class = new \ReflectionClass($name); @@ -215,12 +246,12 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) } elseif ($class->isTrait()) { $modifier = 'trait'; } - throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name); + throw new \Error('Cannot instantiate ' . $modifier . ' '. $name); } // build list of constructor parameters based on parameter types $ctor = $class->getConstructor(); - $params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false); + $params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false, ''); // instantiate with list of parameters // @phpstan-ignore-next-line because `$class->newInstance()` is known to return `T` @@ -229,13 +260,15 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) /** * @return list - * @throws \BadMethodCallException if either parameter can not be loaded + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Error if either parameter can not be loaded + * @throws \Throwable if container factory function throws unexpected exception */ - private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array + private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables, string $for): array { $params = []; foreach ($function->getParameters() as $parameter) { - $params[] = $this->loadParameter($parameter, $depth, $allowVariables); + $params[] = $this->loadParameter($parameter, $depth, $allowVariables, $for); } return $params; @@ -243,57 +276,66 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $ /** * @return mixed - * @throws \BadMethodCallException if $parameter can not be loaded + * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Error if $parameter can not be loaded + * @throws \Throwable if container factory function throws unexpected exception */ - private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */ + private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables, string $for) /*: mixed (PHP 8.0+) */ { - assert(\is_array($this->container)); + // abort for unreasonably deep nesting or recursive types + if ($depth < 1) { + throw new \Error(self::parameterError($parameter, $for) . ' is recursive'); + } + assert(\is_array($this->container)); $type = $parameter->getType(); - $hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull()); // abort for union types (PHP 8.0+) and intersection types (PHP 8.1+) // @phpstan-ignore-next-line for PHP < 8 if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart - if ($hasDefault) { - return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + if ($type->allowsNull()) { + return null; } - throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); + + throw new \Error( + self::parameterError($parameter, $for) . ' expects unsupported type ' . $type + ); } // @codeCoverageIgnoreEnd // load container variables if parameter name is known assert($type === null || $type instanceof \ReflectionNamedType); if ($allowVariables && $this->hasVariable($parameter->getName())) { - return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth); - } + $value = $this->loadVariable($parameter->getName(), $depth); - // abort if parameter is untyped and not explicitly defined by container variable - if ($type === null) { - assert($parameter->allowsNull()); - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); + // skip type checks and allow all values if expected type is undefined or mixed (PHP 8+) + // allow null values if parameter is marked nullable or untyped or mixed + if ($type === null || ($value === null && $parameter->allowsNull()) || $type->getName() === 'mixed' || $this->validateType($value, $type)) { + return $value; } - throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type'); - } - // use default/nullable argument if not loadable as container variable or by type - assert($type instanceof \ReflectionNamedType); - if ($hasDefault && ($type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))) { - return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + throw new \TypeError( + self::parameterError($parameter, $for) . ' must be of type ' . self::typeName($type) . ', ' . $this->gettype($value) . ' given' + ); } - // abort if required container variable is not defined or for any other primitive types (array etc.) - if ($type->isBuiltin()) { - if ($allowVariables) { - throw new \BadMethodCallException(self::parameterError($parameter) . ' is not defined'); - } else { - throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName()); + // use default/nullable argument if not loadable as container variable or by type + if (!$type instanceof \ReflectionNamedType || $type->isBuiltin() || !\array_key_exists($type->getName(), $this->container)) { + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + if ($type !== null && $type->allowsNull() && $type->getName() !== 'mixed') { + return null; } } - // abort for unreasonably deep nesting or recursive types - if ($depth < 1) { - throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive'); + // abort if required container variable is not defined or for any other primitive types (array etc.) + if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) { + throw new \Error( + self::parameterError($parameter, $for) . ' requires container config' . ($type !== null ? ' with type ' . self::typeName($type) : '') . ', none given' + ); } // @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition @@ -307,29 +349,29 @@ private function hasVariable(string $name): bool /** * @return object|string|int|float|bool|null - * @throws \BadMethodCallException if $name is not a valid container variable + * @throws \TypeError if container factory returns an unexpected type + * @throws \Error if $name can not be loaded + * @throws \Throwable if container factory function throws unexpected exception */ - private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */ + private function loadVariable(string $name, int $depth = 64) /*: object|string|int|float|bool|null (PHP 8.0+) */ { assert($this->hasVariable($name)); assert(\is_array($this->container) || !$this->container->has($name)); if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) { - if ($depth < 1) { - throw new \BadMethodCallException('Container variable $' . $name . ' is recursive'); - } - // build list of factory parameters based on parameter types $factory = $this->container[$name]; assert($factory instanceof \Closure); $closure = new \ReflectionFunction($factory); - $params = $this->loadFunctionParams($closure, $depth - 1, true); + $params = $this->loadFunctionParams($closure, $depth - 1, true, '$' . $name); // invoke factory with list of parameters $value = $params === [] ? $factory() : $factory(...$params); if (!\is_object($value) && !\is_scalar($value) && $value !== null) { - throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . $this->gettype($value)); + throw new \TypeError( + 'Return value of ' . self::functionName($closure) . ' for $' . $name . ' must be of type object|string|int|float|bool|null, ' . $this->gettype($value) . ' returned' + ); } $this->container[$name] = $value; @@ -347,41 +389,54 @@ private function loadVariable(string $name, string $type, bool $nullable, int $d } assert(\is_object($value) || \is_scalar($value) || $value === null); - - // allow null values if parameter is marked nullable or untyped or mixed - if ($nullable && $value === null) { - return null; - } - - // skip type checks and allow all values if expected type is undefined or mixed (PHP 8+) - if ($type === 'mixed') { - return $value; - } - - if ( - (\is_object($value) && !$value instanceof $type) || - (!\is_object($value) && !\in_array($type, ['string', 'int', 'float', 'bool'])) || - ($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value)) - ) { - throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . $this->gettype($value)); - } - return $value; } + /** + * @param object|string|int|float|bool|null $value + * @param \ReflectionNamedType $type + * @throws void + */ + private function validateType($value, \ReflectionNamedType $type): bool + { + $type = $type->getName(); + return ( + (\is_object($value) && $value instanceof $type) || + (\is_string($value) && $type === 'string') || + (\is_int($value) && $type === 'int') || + (\is_float($value) && $type === 'float') || + (\is_bool($value) && $type === 'bool') + ); + } + /** @throws void */ - private static function parameterError(\ReflectionParameter $parameter): string + private static function functionName(\ReflectionFunctionAbstract $function): string { - $function = $parameter->getDeclaringFunction(); $name = $function->getShortName(); if ($name[0] === '{') { // $function->isAnonymous() (PHP 8.2+) // use PHP 8.4+ format including closure file and line on all PHP versions: https://3v4l.org/tAs7s $name = '{closure:' . $function->getFileName() . ':' . $function->getStartLine() . '}'; - } elseif (($class = $parameter->getDeclaringClass()) !== null) { - $name = explode("\0", $class->getName())[0] . '::' . $name; + } elseif ($function instanceof \ReflectionMethod && ($class = $function->getDeclaringClass()) !== null) { + $name = \explode("\0", $class->getName())[0] . '::' . $name; } + return $name . '()'; + } - return 'Argument #' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . $name . '()'; + /** @throws void */ + private static function parameterError(\ReflectionParameter $parameter, string $for): string + { + return 'Argument #' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . self::functionName($parameter->getDeclaringFunction()) . ($for !== '' ? ' for ' . $for : ''); + } + + /** + * @param \ReflectionNamedType $type + * @return string + * @throws void + * @see https://www.php.net/manual/en/reflectiontype.tostring.php (PHP 8+) + */ + private static function typeName(\ReflectionNamedType $type): string + { + return ($type->allowsNull() && $type->getName() !== 'mixed' ? '?' : '') . $type->getName(); } /** @@ -401,6 +456,6 @@ private function gettype($value): string } elseif ($value === null) { return 'null'; } - return \is_object($value) ? \get_class($value) : \gettype($value); + return \is_object($value) ? \explode("\0", \get_class($value))[0] : \gettype($value); } } diff --git a/tests/AppTest.php b/tests/AppTest.php index bb421e6..f16189b 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1605,7 +1605,7 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught BadMethodCallException with message Request handler class UnknownClass not found in Container.php:%d.

\n%a", (string) $response->getBody()); + $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught Error with message Request handler class UnknownClass failed to load: Class UnknownClass not found in Container.php:%d.

\n%a", (string) $response->getBody()); } public function provideInvalidClasses(): \Generator @@ -1637,12 +1637,12 @@ public function provideInvalidClasses(): \Generator yield [ InvalidConstructorUntyped::class, - 'Argument #1 ($value) of %s::__construct() has no type' + 'Argument #1 ($value) of %s::__construct() requires container config, none given' ]; yield [ InvalidConstructorInt::class, - 'Argument #1 ($value) of %s::__construct() expects unsupported type int' + 'Argument #1 ($value) of %s::__construct() requires container config with type int, none given' ]; if (PHP_VERSION_ID >= 80000) { @@ -1692,7 +1692,7 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught BadMethodCallException with message Request handler class " . $class . " failed to load: $error in Container.php:%d.

\n%a", (string) $response->getBody()); + $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught Error with message Request handler class $class failed to load: $error in Container.php:%d.

\n%a", (string) $response->getBody()); } public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhenHandlerClassRequiresUnexpectedCallableParameter(): void @@ -1739,7 +1739,7 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe $this->assertStringContainsString("Error 500: Internal Server Error\n", (string) $response->getBody()); $this->assertStringContainsString("

The requested page failed to load, please try again later.

\n", (string) $response->getBody()); - $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught BadMethodCallException with message Request handler class %s has no public __invoke() method in Container.php:%d.

\n%a", (string) $response->getBody()); + $this->assertStringMatchesFormat("%a

Expected request handler to return Psr\Http\Message\ResponseInterface but got uncaught Error with message Request handler class@anonymous has no public __invoke() method in Container.php:%d.

\n%a", (string) $response->getBody()); } public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhenHandlerReturnsPromiseWhichFulfillsWithWrongValue(): void diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 31aabb4..c63a130 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -1264,6 +1264,34 @@ public function __invoke(): ResponseInterface $this->assertEquals('"bar"', (string) $response->getBody()); } + public function testCallableReturnsCallableThatThrowsForUnknownClass(): void + { + $request = new ServerRequest('GET', 'http://example.com/'); + + $container = new Container([]); + + $callable = $container->callable('UnknownClass'); // @phpstan-ignore-line + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Request handler class UnknownClass failed to load: Class UnknownClass not found'); + $callable($request); + } + + public function testCallableReturnsCallableThatThrowsForNonCallableClass(): void + { + $request = new ServerRequest('GET', 'http://example.com/'); + + $controller = new class() { }; + + $container = new Container([]); + + $callable = $container->callable(get_class($controller)); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Request handler class@anonymous has no public __invoke() method'); + $callable($request); + } + public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesUnknownVariable(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -1284,8 +1312,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($username) of {closure:' . __FILE__ . ':' . $line .'}() is not defined'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($username) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass requires container config with type string, none given'); $callable($request); } @@ -1300,6 +1328,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (string $stdClass) { return (object) ['name' => $stdClass]; @@ -1308,8 +1337,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $stdClass is recursive'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line .'}() for $stdClass is recursive'); $callable($request); } @@ -1324,6 +1353,7 @@ public function __construct(string $stdClass) } }; + $line = __LINE__ + 2; $container = new Container([ get_class($controller) => function (string $stdClass) use ($controller) { $class = get_class($controller); @@ -1334,8 +1364,8 @@ public function __construct(string $stdClass) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $stdClass expected type string, but got stdClass'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line . '}() for class@anonymous must be of type string, stdClass given'); $callable($request); } @@ -1350,6 +1380,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 5; $container = new Container([ \stdClass::class => function (string $http) { return (object) ['name' => $http]; @@ -1361,8 +1392,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $http expected type object|scalar|null from factory, but got resource'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for $http must be of type object|string|int|float|bool|null, resource returned'); $callable($request); } @@ -1377,6 +1408,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (\stdClass $http) { return (object) ['name' => $http]; @@ -1386,8 +1418,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $http expected type stdClass, but got int'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($http) of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type stdClass, int given'); $callable($request); } @@ -1402,6 +1434,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (string $http) { return (object) ['name' => $http]; @@ -1411,8 +1444,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $http expected type string, but got int'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($http) of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type string, int given'); $callable($request); } @@ -1427,6 +1460,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (int $http) { return (object) ['name' => $http]; @@ -1436,8 +1470,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $http expected type int, but got string'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($http) of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type int, string given'); $callable($request); } @@ -1452,6 +1486,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (float $percent) { return (object) ['percent' => $percent]; @@ -1461,8 +1496,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $percent expected type float, but got string'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($percent) of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type float, string given'); $callable($request); } @@ -1477,6 +1512,7 @@ public function __construct(\stdClass $data) } }; + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function (bool $admin) { return (object) ['admin' => $admin]; @@ -1486,8 +1522,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Container variable $admin expected type bool, but got string'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($admin) of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type bool, string given'); $callable($request); } @@ -1508,7 +1544,7 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); + $this->expectException(\Error::class); $this->expectExceptionMessage('Class Yes not found'); $callable($request); } @@ -1530,7 +1566,7 @@ public function __construct(?\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); + $this->expectException(\Error::class); $this->expectExceptionMessage('Class Yes not found'); $callable($request); } @@ -1552,8 +1588,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for stdClass contains unexpected int'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::loadObject() for stdClass must be of type stdClass, int returned'); $callable($request); } @@ -1574,8 +1610,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for stdClass contains unexpected null'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::loadObject() for stdClass must be of type stdClass, null returned'); $callable($request); } @@ -1596,8 +1632,8 @@ public function __construct(?\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for stdClass contains unexpected null'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::loadObject() for stdClass must be of type stdClass, null returned'); $callable($request); } @@ -1618,8 +1654,8 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for stdClass contains unexpected React\Http\Message\Response'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::loadObject() for stdClass must be of type stdClass, React\Http\Message\Response returned'); $callable($request); } @@ -1640,51 +1676,71 @@ public function __construct(string $name) $callable = $container->callable(get_class($controller)); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($name) of class@anonymous::__construct() expects unsupported type string'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($name) of class@anonymous::__construct() requires container config with type string, none given'); $callable($request); } - public function testCtorThrowsWhenMapContainsInvalidArray(): void + public function testCtorThrowsWhenConfigContainsInvalidArray(): void { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for all contains unexpected array'); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "all" must be of type object|string|int|float|bool|null|Closure, array given'); new Container([ // @phpstan-ignore-line 'all' => [] ]); } - public function testCtorThrowsWhenMapContainsInvalidResource(): void + public function testCtorThrowsWhenConfigContainsInvalidResource(): void { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for file contains unexpected resource'); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "file" must be of type object|string|int|float|bool|null|Closure, resource given'); new Container([ // @phpstan-ignore-line 'file' => tmpfile() ]); } - public function testCtorThrowsWhenMapForClassContainsInvalidObject(): void + public function testCtorThrowsWhenConfigForClassContainsInvalidObject(): void { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for Psr\Http\Message\ResponseInterface contains unexpected stdClass'); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "Psr\Http\Message\ResponseInterface" must be of type Psr\Http\Message\ResponseInterface|Closure|string, stdClass given'); new Container([ ResponseInterface::class => new \stdClass() ]); } - public function testCtorThrowsWhenMapForClassContainsInvalidNull(): void + public function testCtorThrowsWhenConfigForClassContainsInvalidAnonymousClass(): void { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Map for Psr\Http\Message\ResponseInterface contains unexpected null'); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "Psr\Http\Message\ResponseInterface" must be of type Psr\Http\Message\ResponseInterface|Closure|string, class@anonymous given'); + + new Container([ + ResponseInterface::class => new class() { } + ]); + } + + public function testCtorThrowsWhenConfigForClassContainsInvalidNull(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "Psr\Http\Message\ResponseInterface" must be of type Psr\Http\Message\ResponseInterface|Closure|string, null given'); new Container([ ResponseInterface::class => null ]); } + public function testCtorThrowsWhenConfigForClassContainsInvalidResource(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($config) for key "Psr\Http\Message\ResponseInterface" must be of type Psr\Http\Message\ResponseInterface|Closure|string, resource given'); + + new Container([ // @phpstan-ignore-line + ResponseInterface::class => tmpfile() + ]); + } + public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidClassName(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -1695,7 +1751,7 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidCl $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); + $this->expectException(\Error::class); $this->expectExceptionMessage('Class invalid not found'); $callable($request); } @@ -1704,14 +1760,15 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidIn { $request = new ServerRequest('GET', 'http://example.com/'); + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function () { return 42; } ]); $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Factory for stdClass returned unexpected int'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type stdClass, int returned'); $callable($request); } @@ -1725,8 +1782,8 @@ public function testCallableReturnsCallableThatThrowsWhenMapReferencesClassNameT $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Factory for stdClass returned unexpected React\Http\Message\Response'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::loadObject() for stdClass must be of type stdClass, React\Http\Message\Response returned'); $callable($request); } @@ -1734,14 +1791,15 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsClassName { $request = new ServerRequest('GET', 'http://example.com/'); + $line = __LINE__ + 2; $container = new Container([ \stdClass::class => function () { return Response::class; } ]); $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Factory for stdClass returned unexpected React\Http\Message\Response'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for stdClass must be of type stdClass, React\Http\Message\Response returned'); $callable($request); } @@ -1755,7 +1813,7 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresInvalidC $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); + $this->expectException(\Error::class); $this->expectExceptionMessage('Class self not found'); $callable($request); } @@ -1771,8 +1829,8 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresUntypedA $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($undefined) of {closure:' . __FILE__ . ':' . $line .'}() has no type'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($undefined) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass requires container config, none given'); $callable($request); } @@ -1790,8 +1848,8 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresUndefine $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($undefined) of {closure:' . __FILE__ . ':' . $line .'}() is not defined'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($undefined) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass requires container config with type mixed, none given'); $callable($request); } @@ -1806,8 +1864,8 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresRecursiv $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($data) of {closure:' . __FILE__ . ':' . $line .'}() is recursive'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($data) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass is recursive'); $callable($request); } @@ -1821,8 +1879,8 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryIsRecursive(): v $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Factory for stdClass is recursive'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Container config for stdClass is recursive'); $callable($request); } @@ -1838,8 +1896,8 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryIsRecursiveClass $callable = $container->callable(\stdClass::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Factory for stdClass is recursive'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Container config for stdClass is recursive'); $callable($request); } @@ -1884,7 +1942,7 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidCl $callable = $container->callable('FooBar'); // @phpstan-ignore-line - $this->expectException(\BadMethodCallException::class); + $this->expectException(\Error::class); $this->expectExceptionMessage('Request handler class FooBar failed to load: Unable to load class'); $callable($request); } @@ -2080,6 +2138,46 @@ public function testGetEnvReturnsStringFromGlobalEnvBeforeProcessEnvIfPsrContain $this->assertEquals('foo', $ret); } + public function testGetEnvThrowsIfFactoryFunctionThrows(): void + { + $container = new Container([ + 'X_FOO' => function () { + throw new \RuntimeException('Demo'); + } + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Demo'); + $container->getEnv('X_FOO'); + } + + public function testGetEnvThrowsIfFactoryFunctionReturnsInvalidResource(): void + { + $line = __LINE__ + 2; + $container = new Container([ + 'X_FOO' => function () { + return tmpfile(); + } + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO must be of type object|string|int|float|bool|null, resource returned'); + $container->getEnv('X_FOO'); + } + + public function testGetEnvThrowsIfFactoryFunctionReturnsInvalidInt(): void + { + $container = new Container([ + 'X_FOO' => function () { + return 0; + } + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Return value of FrameworkX\Container::getEnv() for $X_FOO must be of type string|null, int returned'); + $container->getEnv('X_FOO'); + } + public function testGetEnvThrowsIfMapContainsInvalidType(): void { $container = new Container([ @@ -2087,7 +2185,7 @@ public function testGetEnvThrowsIfMapContainsInvalidType(): void ]); $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Environment variable $X_FOO expected type string|null, but got false'); + $this->expectExceptionMessage('Return value of ' . Container::class . '::getEnv() for $X_FOO must be of type string|null, false returned'); $container->getEnv('X_FOO'); } @@ -2100,8 +2198,8 @@ public function testGetEnvThrowsWhenFactoryUsesBuiltInFunctionThatReferencesUnkn 'X_FOO' => \Closure::fromCallable('extension_loaded') ]); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($extension) of extension_loaded() is not defined'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($extension) of extension_loaded() for $X_FOO requires container config with type string, none given'); $container->getEnv('X_FOO'); } @@ -2118,8 +2216,65 @@ public function foo(string $bar): string 'X_FOO' => \Closure::fromCallable([$class, 'foo']) ]); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Argument #1 ($bar) of class@anonymous::foo() is not defined'); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($bar) of class@anonymous::foo() for $X_FOO requires container config with type string, none given'); + $container->getEnv('X_FOO'); + } + + public function testGetEnvThrowsWhenFactoryFunctionExpectsRequiredEnvVariableButNoneGiven(): void + { + $line = __LINE__ + 2; + $container = new Container([ + 'X_FOO' => function (string $X_UNDEFINED) { return (string) $X_UNDEFINED; }, + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($X_UNDEFINED) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO requires container config with type string, none given'); + $container->getEnv('X_FOO'); + } + + public function testGetEnvThrowsWhenFactoryFunctionExpectsNullableIntArgumentButGivenString(): void + { + $line = __LINE__ + 2; + $container = new Container([ + 'X_FOO' => function (?int $bar) { return (string) $bar; }, + 'bar' => 'bar' + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($bar) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO must be of type ?int, string given'); + $container->getEnv('X_FOO'); + } + + /** + * @requires PHP 8 + */ + public function testGetEnvThrowsWhenFactoryFunctionExpectsUnsupportedUnionType(): void + { + $line = __LINE__ + 2; + $fn = null; + $fn = #[PHP8] function (string|int $X_UNION) { return (string) $X_UNION; }; + $container = new Container([ + 'X_FOO' => $fn, + 'X_UNION' => 42 + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($X_UNION) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO expects unsupported type string|int'); + $container->getEnv('X_FOO'); + } + + /** @link https://3v4l.org/VaFMd */ + public function testGetEnvThrowsWhenFactoryFunctionExpectsIntArgumentButGivenAnonymousClass(): void + { + $line = __LINE__ + 2; + $container = new Container([ + 'X_FOO' => function (int $bar) { return (string) $bar; }, + 'bar' => new class { } + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($bar) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO must be of type int, class@anonymous given'); $container->getEnv('X_FOO'); } @@ -2133,7 +2288,7 @@ public function testGetEnvThrowsIfMapPsrContainerReturnsInvalidType(): void $container = new Container($psr); $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Environment variable $X_FOO expected type string|null, but got int'); + $this->expectExceptionMessage('Return value of ' . Container::class .'::getEnv() for $X_FOO must be of type string|null, int returned'); $container->getEnv('X_FOO'); } @@ -2189,6 +2344,68 @@ public function testGetAccessLogHandlerReturnsDefaultAccessLogHandlerInstanceIfP $this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler); } + public function testGetAccessLogHandlerThrowsIfFactoryFunctionThrows(): void + { + $container = new Container([ + AccessLogHandler::class => function () { + throw new \RuntimeException('Demo'); + } + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Demo'); + $container->getAccessLogHandler(); + } + + public function testGetAccessLogHandlerThrowsIfFactoryFunctionReturnsInvalidValue(): void + { + $line = __LINE__ + 2; + $container = new Container([ + AccessLogHandler::class => function () { + return null; + } + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for FrameworkX\AccessLogHandler must be of type FrameworkX\AccessLogHandler, null returned'); + $container->getAccessLogHandler(); + } + + public function testGetAccessLogHandlerThrowsIfConfigIsRecursive(): void + { + $container = new Container([ + AccessLogHandler::class => AccessLogHandler::class + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Container config for FrameworkX\AccessLogHandler is recursive'); + $container->getAccessLogHandler(); + } + + public function testGetAccessLogHandlerThrowsIfFactoryFunctionIsRecursive(): void + { + $container = new Container([ + AccessLogHandler::class => function () { + return AccessLogHandler::class; + } + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Container config for FrameworkX\AccessLogHandler is recursive'); + $container->getAccessLogHandler(); + } + + public function testGetAccessLogHandlerThrowsIfConfigReferencesInterface(): void + { + $container = new Container([ + AccessLogHandler::class => \Iterator::class + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Cannot instantiate interface Iterator'); + $container->getAccessLogHandler(); + } + public function testGetErrorHandlerReturnsDefaultErrorHandlerInstance(): void { $container = new Container([]); @@ -2241,6 +2458,33 @@ public function testGetErrorHandlerReturnsDefaultErrorHandlerInstanceIfPsrContai $this->assertInstanceOf(ErrorHandler::class, $errorHandler); } + public function testGetErrorHandlerThrowsIfFactoryFunctionThrows(): void + { + $container = new Container([ + ErrorHandler::class => function () { + throw new \RuntimeException('Demo'); + } + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Demo'); + $container->getErrorHandler(); + } + + public function testGetErrorHandlerThrowsIfFactoryFunctionReturnsInvalidValue(): void + { + $line = __LINE__ + 2; + $container = new Container([ + ErrorHandler::class => function () { + return null; + } + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for FrameworkX\ErrorHandler must be of type FrameworkX\ErrorHandler, null returned'); + $container->getErrorHandler(); + } + public function testInvokeContainerAsMiddlewareReturnsFromNextRequestHandler(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -2295,7 +2539,7 @@ public static function provideInvalidContainerConfigValues(): \Generator public function testCtorWithInvalidValueThrows($value, string $type): void { $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . $type . ' given'); + $this->expectExceptionMessage('Argument #1 ($config) must be of type array|Psr\Container\ContainerInterface, ' . $type . ' given'); new Container($value); // @phpstan-ignore-line } }