diff --git a/docs/best-practices/controllers.md b/docs/best-practices/controllers.md index be304f9..b1cd489 100644 --- a/docs/best-practices/controllers.md +++ b/docs/best-practices/controllers.md @@ -282,9 +282,9 @@ $app = new FrameworkX\App($container); Factory functions used in the container configuration map may also reference variables defined in the container configuration. You may use any object or -scalar or `null` value for container variables or factory functions that return -any such value. This can be particularly useful when combining autowiring with -some manual configuration like this: +scalar or `array` or `null` value for container variables or factory functions +that return any such value. This can be particularly useful when combining +autowiring with some manual configuration like this: === "Scalar values" diff --git a/src/Container.php b/src/Container.php index 015db96..10dcc70 100644 --- a/src/Container.php +++ b/src/Container.php @@ -14,14 +14,14 @@ */ class Container { - /** @var array|ContainerInterface */ + /** @var array|null)|scalar|array|null>|ContainerInterface */ private $container; /** @var bool */ private $useProcessEnv; /** - * @param array|ContainerInterface $config + * @param array|null) | object | scalar | array | null>|ContainerInterface $config * @throws \TypeError if given $config is invalid */ public function __construct($config = []) @@ -39,9 +39,9 @@ public function __construct($config = []) '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) { + if (!\is_object($value) && !\is_scalar($value) && !\is_array($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' + 'Argument #1 ($config) for key "' . $name . '" must be of type object|string|int|float|bool|array|null|Closure, ' . $this->gettype($value) . ' given' ); } } @@ -385,12 +385,12 @@ private function hasVariable(string $name): bool } /** - * @return object|string|int|float|bool|null + * @return object|string|int|float|bool|array|null * @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, int $depth = 64) /*: object|string|int|float|bool|null (PHP 8.0+) */ + private function loadVariable(string $name, int $depth = 64) /*: object|string|int|float|bool|array|null (PHP 8.0+) */ { \assert($this->hasVariable($name)); \assert(\is_array($this->container) || !$this->container->has($name)); @@ -409,9 +409,9 @@ private function loadVariable(string $name, int $depth = 64) /*: object|string|i $this->container[$name] = $factory; } - if (!\is_object($value) && !\is_scalar($value) && $value !== null) { + if (!\is_object($value) && !\is_scalar($value) && !\is_array($value) && $value !== null) { 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' + 'Return value of ' . self::functionName($closure) . ' for $' . $name . ' must be of type object|string|int|float|bool|array|null, ' . $this->gettype($value) . ' returned' ); } elseif ($value instanceof \Closure) { throw new \TypeError( @@ -433,12 +433,12 @@ private function loadVariable(string $name, int $depth = 64) /*: object|string|i \assert($this->useProcessEnv && $value !== false); } - \assert(\is_object($value) || \is_scalar($value) || $value === null); + \assert(\is_object($value) || \is_scalar($value) || \is_array($value) || $value === null); return $value; } /** - * @param object|string|int|float|bool|null $value + * @param object|string|int|float|bool|array|null $value * @param \ReflectionType $type * @throws void */ @@ -470,6 +470,7 @@ private function validateType($value, \ReflectionType $type): bool (\is_int($value) && $type === 'int') || (\is_float($value) && $type === 'float') || (\is_bool($value) && $type === 'bool') || + (\is_array($value) && $type === 'array') || (\is_iterable($value) && $type === 'iterable') || (\is_callable($value) && $type === 'callable') || ($value === true && $type === 'true') || // PHP 8.2+ standalone type diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 654b963..7b57287 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -542,7 +542,7 @@ public function __invoke(): ResponseInterface $this->assertEquals('{"name":"Alice"}', (string) $response->getBody()); } - /** @return list> */ + /** @return list|null,string}> */ public function provideMixedValue(): array { return [ @@ -554,6 +554,18 @@ public function provideMixedValue(): array 'Alice', '"Alice"' ], + [ + [], + '[]' + ], + [ + ['a', 'b'], + '["a","b"]' + ], + [ + ['name' => 'Alice'], + '{"name":"Alice"}' + ], [ null, 'null' @@ -563,7 +575,7 @@ public function provideMixedValue(): array /** * @dataProvider provideMixedValue - * @param \stdClass|string|null $value + * @param \stdClass|string|array|null $value */ public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresUntypedContainerVariable($value, string $json): void { @@ -602,7 +614,7 @@ public function __invoke(): ResponseInterface /** * @dataProvider provideMixedValue - * @param \stdClass|string|null $value + * @param \stdClass|string|array|null $value */ public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresUntypedContainerVariableWithFactory($value, string $json): void { @@ -644,7 +656,7 @@ public function __invoke(): ResponseInterface /** * @requires PHP 8 * @dataProvider provideMixedValue - * @param \stdClass|string|null $value + * @param \stdClass|string|array|null $value */ public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresMixedContainerVariable($value, string $json): void { @@ -1360,7 +1372,7 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); $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'); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for $http must be of type object|string|int|float|bool|array|null, resource returned'); $callable($request); } @@ -1648,20 +1660,10 @@ public function __construct(string $name) $callable($request); } - public function testCtorThrowsWhenConfigContainsInvalidArray(): void - { - $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 testCtorThrowsWhenConfigContainsInvalidResource(): void { $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'); + $this->expectExceptionMessage('Argument #1 ($config) for key "file" must be of type object|string|int|float|bool|array|null|Closure, resource given'); new Container([ // @phpstan-ignore-line 'file' => tmpfile() @@ -1937,6 +1939,16 @@ public function testGetEnvReturnsStringFromFactoryFunctionWithObjectType(): void $this->assertEquals('ArrayObject', $container->getEnv('X_FOO')); } + public function testGetEnvReturnsStringFromFactoryFunctionWithArrayType(): void + { + $container = new Container([ + 'X_FOO' => function (array $items) { return implode(',', $items); }, + 'items' => ['a', 'b', 'c'] + ]); + + $this->assertEquals('a,b,c', $container->getEnv('X_FOO')); + } + public function testGetEnvReturnsStringFromFactoryFunctionWithIterableType(): void { $container = new Container([ @@ -1947,6 +1959,16 @@ public function testGetEnvReturnsStringFromFactoryFunctionWithIterableType(): vo $this->assertEquals('123', $container->getEnv('X_FOO')); } + public function testGetEnvReturnsStringFromFactoryFunctionWithIterableTypeAndArrayValue(): void + { + $container = new Container([ + 'X_FOO' => function (iterable $items) { $s = ''; foreach ($items as $v) { $s .= $v; } return $s; }, + 'items' => [1, 2, 3] + ]); + + $this->assertEquals('123', $container->getEnv('X_FOO')); + } + public function testGetEnvReturnsStringFromFactoryFunctionWithCallableType(): void { $container = new Container([ @@ -1957,6 +1979,16 @@ public function testGetEnvReturnsStringFromFactoryFunctionWithCallableType(): vo $this->assertEquals('ALICE', $container->getEnv('X_FOO')); } + public function testGetEnvReturnsStringFromFactoryFunctionWithCallableTypeAndArrayValue(): void + { + $container = new Container([ + 'X_FOO' => function (callable $fn) { return $fn(); }, + 'fn' => [new \DateTimeZone('UTC'), 'getName'] + ]); + + $this->assertEquals('UTC', $container->getEnv('X_FOO')); + } + /** * @requires PHP 8.2 */ @@ -2325,7 +2357,7 @@ public function testGetEnvThrowsIfFactoryFunctionReturnsInvalidResource(): void ]); $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'); + $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO must be of type object|string|int|float|bool|array|null, resource returned'); $container->getEnv('X_FOO'); } @@ -2369,6 +2401,17 @@ public function testGetEnvThrowsIfMapContainsInvalidType(): void $container->getEnv('X_FOO'); } + public function testGetEnvThrowsIfMapContainsInvalidArray(): void + { + $container = new Container([ + 'X_FOO' => ['a', 'b'] + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Return value of ' . Container::class . '::getEnv() for $X_FOO must be of type string|null, array returned'); + $container->getEnv('X_FOO'); + } + /** * @requires PHP 8 */ @@ -2580,6 +2623,19 @@ public function testGetEnvThrowsWhenFactoryFunctionExpectsObjectTypeButWrongType $container->getEnv('X_FOO'); } + public function testGetEnvThrowsWhenFactoryFunctionExpectsArrayTypeButWrongTypeGiven(): void + { + $line = __LINE__ + 2; + $container = new Container([ + 'X_FOO' => function (array $items) { return implode(',', $items); }, + 'items' => 'not-an-array' + ]); + + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument #1 ($items) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO must be of type array, string given'); + $container->getEnv('X_FOO'); + } + public function testGetEnvThrowsWhenFactoryFunctionExpectsIterableTypeButWrongTypeGiven(): void { $line = __LINE__ + 2;