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
2 changes: 1 addition & 1 deletion docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ all uppercase in any factory function like this:
// You may explicitly configure this built-in functionality like this:
// 'X_LISTEN' => '0.0.0.0:8081'
// 'X_LISTEN' => fn(int|string $PORT = 8080) => '0.0.0.0:' . $PORT
'X_LISTEN' => '127.0.0.1:8080'
'X_LISTEN' => fn(string $X_LISTEN = '127.0.0.1:8080') => $X_LISTEN
]);

$app = new FrameworkX\App($container);
Expand Down
14 changes: 10 additions & 4 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,21 @@ private function loadVariable(string $name, int $depth = 64) /*: object|string|i
\assert(\is_array($this->container) || !$this->container->has($name));

if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) {
// 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, '$' . $name);

// invoke factory with list of parameters
$value = $params === [] ? $factory() : $factory(...$params);
// build list of factory parameters based on parameter types
// temporarily unset factory reference to allow loading recursive variables from environment
try {
unset($this->container[$name]);
$params = $this->loadFunctionParams($closure, $depth - 1, true, '$' . $name);
} finally {
$this->container[$name] = $factory;
}

// invoke factory with list of parameters
$value = $factory(...$params);
if (!\is_object($value) && !\is_scalar($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'
Expand Down
93 changes: 92 additions & 1 deletion tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ public function __construct(\stdClass $data)
$callable = $container->callable(get_class($controller));

$this->expectException(\Error::class);
$this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line .'}() for $stdClass is recursive');
$this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line .'}() for $stdClass requires container config with type string, none given');
$callable($request);
}

Expand Down Expand Up @@ -2058,6 +2058,37 @@ public function testGetEnvReturnsStringFromGlobalEnvBeforeProcessEnvIfNotSetInMa
$this->assertEquals('foo', $ret);
}

public function testGetEnvReturnsStringFromRecursiveFactoryReferencingStringFromGlobalEnv(): void
{
$container = new Container([
'X_FOO' => function (string $X_FOO) { return strtoupper($X_FOO); }
]);

$_ENV['X_FOO'] = 'foo';
$ret = $container->getEnv('X_FOO');
unset($_ENV['X_FOO']);

$this->assertEquals('FOO', $ret);
}

public function testGetEnvReturnsStringFromRecursiveFactoryWithNullableValueIfNotSetInGlobalEnv(): void
{
$container = new Container([
'X_FOO' => function (?string $X_FOO = null) { return $X_FOO ?? 'foo'; }
]);

$this->assertEquals('foo', $container->getEnv('X_FOO'));
}

public function testGetEnvReturnsStringFromRecursiveFactoryWithDefaultValueIfNotSetInGlobalEnv(): void
{
$container = new Container([
'X_FOO' => function (string $X_FOO = 'foo') { return $X_FOO; }
]);

$this->assertEquals('foo', $container->getEnv('X_FOO'));
}

public function testGetEnvReturnsStringFromPsrContainer(): void
{
$psr = $this->createMock(ContainerInterface::class);
Expand Down Expand Up @@ -2338,6 +2369,66 @@ public function testGetEnvThrowsWhenFactoryFunctionExpectsDnfTypeButNoneGiven():
$container->getEnv('X_FOO');
}

public function testGetEnvThrowsWhenRecursiveFactoryReferencesUndefinedVariable(): void
{
$line = __LINE__ + 2;
$container = new Container([
'X_UNDEFINED' => function (string $X_UNDEFINED) { return strtoupper($X_UNDEFINED); }
]);

$this->expectException(\Error::class);
$this->expectExceptionMessage('Argument #1 ($X_UNDEFINED) of {closure:' . __FILE__ . ':' . $line . '}() for $X_UNDEFINED requires container config with type string, none given');
$container->getEnv('X_UNDEFINED');
}

public function testGetEnvReturnsStringAfterFirstCallThrowsWhenRecursiveFactoryReferencesVariableDefinedOnlyOnSecondCall(): void
{
$line = __LINE__ + 2;
$container = new Container([
'X_FOO' => function (string $X_FOO) { return strtoupper($X_FOO); }
]);

try {
$container->getEnv('X_FOO');
$this->fail();
} catch (\Error $e) {
$this->assertEquals('Argument #1 ($X_FOO) of {closure:' . __FILE__ . ':' . $line . '}() for $X_FOO requires container config with type string, none given', $e->getMessage());
}

$_ENV['X_FOO'] = 'defined';
$ret = $container->getEnv('X_FOO');
unset($_ENV['X_FOO']);

$this->assertEquals('DEFINED', $ret);
}

public function testGetEnvReturnsStringAfterFirstCallThrowsWhenRecursiveFactoryThrowsOnFirstCall(): void
{
$container = new Container([
'X_FOO' => function (string $X_FOO) {
static $first = true;
if ($first) {
$first = false;
throw new \RuntimeException('First call');
}
return strtoupper($X_FOO);
}
]);

try {
$_ENV['X_FOO'] = 'defined';
$container->getEnv('X_FOO');
$this->fail();
} catch (\RuntimeException $e) {
$this->assertEquals('First call', $e->getMessage());
}

$ret = $container->getEnv('X_FOO');
unset($_ENV['X_FOO']);

$this->assertEquals('DEFINED', $ret);
}

public function testGetEnvThrowsWhenFactoryFunctionExpectsNullableIntArgumentButGivenString(): void
{
$line = __LINE__ + 2;
Expand Down