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
38 changes: 28 additions & 10 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Container
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
private $container;

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

/** @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
public function __construct($loader = [])
{
Expand All @@ -32,6 +35,9 @@ public function __construct($loader = [])
}
}
$this->container = $loader;

// 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 */
Expand Down Expand Up @@ -98,12 +104,12 @@ public function getEnv(string $name): ?string
{
assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1);

if (\is_array($this->container) && \array_key_exists($name, $this->container)) {
$value = $this->loadVariable($name, 'mixed', true, 64);
} elseif ($this->container instanceof ContainerInterface && $this->container->has($name)) {
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);
} else {
$value = $_SERVER[$name] ?? null;
return null;
}

if (!\is_string($value) && $value !== null) {
Expand Down Expand Up @@ -257,7 +263,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool

// load container variables if parameter name is known
assert($type === null || $type instanceof \ReflectionNamedType);
if ($allowVariables && (\array_key_exists($parameter->getName(), $this->container) || (isset($_SERVER[$parameter->getName()]) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $parameter->getName())))) {
if ($allowVariables && $this->hasVariable($parameter->getName())) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth);
}

Expand Down Expand Up @@ -294,15 +300,21 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
return $this->loadObject($type->getName(), $depth - 1);
}

private function hasVariable(string $name): bool
{
return (\is_array($this->container) && \array_key_exists($name, $this->container)) || (isset($_ENV[$name]) || (\is_string($_SERVER[$name] ?? null) || ($this->useProcessEnv && \getenv($name) !== false)) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $name));
}

/**
* @return object|string|int|float|bool|null
* @throws \BadMethodCallException if $name is not a valid container variable
*/
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
{
assert(\is_array($this->container) && (\array_key_exists($name, $this->container) || isset($_SERVER[$name])));
assert($this->hasVariable($name));
assert(\is_array($this->container) || !$this->container->has($name));

if (($this->container[$name] ?? null) instanceof \Closure) {
if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
}
Expand All @@ -321,11 +333,17 @@ private function loadVariable(string $name, string $type, bool $nullable, int $d
}

$this->container[$name] = $value;
} elseif (\array_key_exists($name, $this->container)) {
} elseif (\is_array($this->container) && \array_key_exists($name, $this->container)) {
$value = $this->container[$name];
} else {
assert(isset($_SERVER[$name]) && \is_string($_SERVER[$name]));
} elseif (isset($_ENV[$name])) {
assert(\is_string($_ENV[$name]));
$value = $_ENV[$name];
} elseif (isset($_SERVER[$name])) {
assert(\is_string($_SERVER[$name]));
$value = $_SERVER[$name];
} else {
$value = \getenv($name);
assert($this->useProcessEnv && $value !== false);
}

assert(\is_object($value) || \is_scalar($value) || $value === null);
Expand Down
116 changes: 115 additions & 1 deletion tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2039,6 +2039,17 @@ public function testGetEnvReturnsStringFromMapFactory(): void
$this->assertEquals('bar', $container->getEnv('X_FOO'));
}

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

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

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

public function testGetEnvReturnsStringFromGlobalServerIfNotSetInMap(): void
{
$container = new Container([]);
Expand All @@ -2050,6 +2061,42 @@ public function testGetEnvReturnsStringFromGlobalServerIfNotSetInMap(): void
$this->assertEquals('bar', $ret);
}

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

putenv('X_FOO=bar');
$ret = $container->getEnv('X_FOO');
putenv('X_FOO');

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

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

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

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

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

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

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

public function testGetEnvReturnsStringFromPsrContainer(): void
{
$psr = $this->createMock(ContainerInterface::class);
Expand All @@ -2074,10 +2121,42 @@ public function testGetEnvReturnsNullIfPsrContainerHasNoEntry(): void
$this->assertNull($container->getEnv('X_FOO'));
}

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

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

putenv('X_FOO=bar');
$ret = $container->getEnv('X_FOO');
putenv('X_FOO');

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

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

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

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

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

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

assert($psr instanceof ContainerInterface);
Expand All @@ -2090,6 +2169,41 @@ public function testGetEnvReturnsStringFromGlobalServerIfPsrContainerHasNoEntry(
$this->assertEquals('bar', $ret);
}

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

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

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

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

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

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

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

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

public function testGetEnvThrowsIfMapContainsInvalidType(): void
{
$container = new Container([
Expand Down