diff --git a/src/Container.php b/src/Container.php index 23175ba..2058379 100644 --- a/src/Container.php +++ b/src/Container.php @@ -13,6 +13,9 @@ class Container /** @var array|ContainerInterface */ private $container; + /** @var bool */ + private $useProcessEnv; + /** @param array|ContainerInterface $loader */ public function __construct($loader = []) { @@ -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 */ @@ -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) { @@ -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); } @@ -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'); } @@ -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); diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 26423a4..389757c 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -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([]); @@ -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); @@ -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); @@ -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([