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
6 changes: 3 additions & 3 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
21 changes: 11 additions & 10 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
*/
class Container
{
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
/** @var array<string,object|callable():(object|scalar|array<mixed>|null)|scalar|array<mixed>|null>|ContainerInterface */
Comment thread
clue marked this conversation as resolved.
private $container;

/** @var bool */
private $useProcessEnv;

/**
* @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $config
* @param array<string,callable():(object|scalar|array<mixed>|null) | object | scalar | array<mixed> | null>|ContainerInterface $config
Comment thread
clue marked this conversation as resolved.
* @throws \TypeError if given $config is invalid
*/
public function __construct($config = [])
Expand All @@ -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'
);
}
}
Expand Down Expand Up @@ -385,12 +385,12 @@ private function hasVariable(string $name): bool
}

/**
* @return object|string|int|float|bool|null
* @return object|string|int|float|bool|array<mixed>|null
Comment thread
clue marked this conversation as resolved.
* @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));
Expand All @@ -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(
Expand All @@ -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<mixed>|null $value
Comment thread
clue marked this conversation as resolved.
* @param \ReflectionType $type
* @throws void
*/
Expand Down Expand Up @@ -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
Expand Down
90 changes: 73 additions & 17 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ public function __invoke(): ResponseInterface
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
}

/** @return list<list<\stdClass|string|null>> */
/** @return list<array{\stdClass|string|array<mixed>|null,string}> */
Comment thread
clue marked this conversation as resolved.
public function provideMixedValue(): array
{
return [
Expand All @@ -554,6 +554,18 @@ public function provideMixedValue(): array
'Alice',
'"Alice"'
],
[
[],
'[]'
],
[
['a', 'b'],
'["a","b"]'
],
[
['name' => 'Alice'],
'{"name":"Alice"}'
],
[
null,
'null'
Expand All @@ -563,7 +575,7 @@ public function provideMixedValue(): array

/**
* @dataProvider provideMixedValue
* @param \stdClass|string|null $value
* @param \stdClass|string|array<mixed>|null $value
Comment thread
clue marked this conversation as resolved.
*/
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresUntypedContainerVariable($value, string $json): void
{
Expand Down Expand Up @@ -602,7 +614,7 @@ public function __invoke(): ResponseInterface

/**
* @dataProvider provideMixedValue
* @param \stdClass|string|null $value
* @param \stdClass|string|array<mixed>|null $value
Comment thread
clue marked this conversation as resolved.
*/
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresUntypedContainerVariableWithFactory($value, string $json): void
{
Expand Down Expand Up @@ -644,7 +656,7 @@ public function __invoke(): ResponseInterface
/**
* @requires PHP 8
* @dataProvider provideMixedValue
* @param \stdClass|string|null $value
* @param \stdClass|string|array<mixed>|null $value
Comment thread
clue marked this conversation as resolved.
*/
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresMixedContainerVariable($value, string $json): void
{
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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([
Expand All @@ -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([
Expand All @@ -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
*/
Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down