Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load environment variables from $_ENV, $_SERVER and getenv() if in thread-safe environment #205

Merged
merged 3 commits into from
Nov 18, 2022
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