Skip to content

Commit a477f66

Browse files
authored
Merge pull request #205 from clue-labs/process-env
Load environment variables from `$_ENV`, `$_SERVER` and `getenv()` if in thread-safe environment
2 parents 1875a97 + 55fe66c commit a477f66

File tree

2 files changed

+143
-11
lines changed

2 files changed

+143
-11
lines changed

src/Container.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ class Container
1313
/** @var array<string,object|callable():(object|scalar|null)|scalar|null>|ContainerInterface */
1414
private $container;
1515

16+
/** @var bool */
17+
private $useProcessEnv;
18+
1619
/** @param array<string,callable():(object|scalar|null) | object | scalar | null>|ContainerInterface $loader */
1720
public function __construct($loader = [])
1821
{
@@ -32,6 +35,9 @@ public function __construct($loader = [])
3235
}
3336
}
3437
$this->container = $loader;
38+
39+
// prefer reading environment from `$_ENV` and `$_SERVER`, only fall back to `getenv()` in thread-safe environments
40+
$this->useProcessEnv = \ZEND_THREAD_SAFE === false || \in_array(\PHP_SAPI, ['cli', 'cli-server', 'cgi-fcgi', 'fpm-fcgi'], true);
3541
}
3642

3743
/** @return mixed */
@@ -98,12 +104,12 @@ public function getEnv(string $name): ?string
98104
{
99105
assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1);
100106

101-
if (\is_array($this->container) && \array_key_exists($name, $this->container)) {
102-
$value = $this->loadVariable($name, 'mixed', true, 64);
103-
} elseif ($this->container instanceof ContainerInterface && $this->container->has($name)) {
107+
if ($this->container instanceof ContainerInterface && $this->container->has($name)) {
104108
$value = $this->container->get($name);
109+
} elseif ($this->hasVariable($name)) {
110+
$value = $this->loadVariable($name, 'mixed', true, 64);
105111
} else {
106-
$value = $_SERVER[$name] ?? null;
112+
return null;
107113
}
108114

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

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

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

303+
private function hasVariable(string $name): bool
304+
{
305+
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));
306+
}
307+
297308
/**
298309
* @return object|string|int|float|bool|null
299310
* @throws \BadMethodCallException if $name is not a valid container variable
300311
*/
301312
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
302313
{
303-
assert(\is_array($this->container) && (\array_key_exists($name, $this->container) || isset($_SERVER[$name])));
314+
assert($this->hasVariable($name));
315+
assert(\is_array($this->container) || !$this->container->has($name));
304316

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

323335
$this->container[$name] = $value;
324-
} elseif (\array_key_exists($name, $this->container)) {
336+
} elseif (\is_array($this->container) && \array_key_exists($name, $this->container)) {
325337
$value = $this->container[$name];
326-
} else {
327-
assert(isset($_SERVER[$name]) && \is_string($_SERVER[$name]));
338+
} elseif (isset($_ENV[$name])) {
339+
assert(\is_string($_ENV[$name]));
340+
$value = $_ENV[$name];
341+
} elseif (isset($_SERVER[$name])) {
342+
assert(\is_string($_SERVER[$name]));
328343
$value = $_SERVER[$name];
344+
} else {
345+
$value = \getenv($name);
346+
assert($this->useProcessEnv && $value !== false);
329347
}
330348

331349
assert(\is_object($value) || \is_scalar($value) || $value === null);

tests/ContainerTest.php

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,17 @@ public function testGetEnvReturnsStringFromMapFactory(): void
20392039
$this->assertEquals('bar', $container->getEnv('X_FOO'));
20402040
}
20412041

2042+
public function testGetEnvReturnsStringFromGlobalEnvIfNotSetInMap(): void
2043+
{
2044+
$container = new Container([]);
2045+
2046+
$_ENV['X_FOO'] = 'bar';
2047+
$ret = $container->getEnv('X_FOO');
2048+
unset($_ENV['X_FOO']);
2049+
2050+
$this->assertEquals('bar', $ret);
2051+
}
2052+
20422053
public function testGetEnvReturnsStringFromGlobalServerIfNotSetInMap(): void
20432054
{
20442055
$container = new Container([]);
@@ -2050,6 +2061,42 @@ public function testGetEnvReturnsStringFromGlobalServerIfNotSetInMap(): void
20502061
$this->assertEquals('bar', $ret);
20512062
}
20522063

2064+
public function testGetEnvReturnsStringFromProcessEnvIfNotSetInMap(): void
2065+
{
2066+
$container = new Container([]);
2067+
2068+
putenv('X_FOO=bar');
2069+
$ret = $container->getEnv('X_FOO');
2070+
putenv('X_FOO');
2071+
2072+
$this->assertEquals('bar', $ret);
2073+
}
2074+
2075+
public function testGetEnvReturnsStringFromGlobalEnvBeforeServerIfNotSetInMap(): void
2076+
{
2077+
$container = new Container([]);
2078+
2079+
$_ENV['X_FOO'] = 'foo';
2080+
$_SERVER['X_FOO'] = 'bar';
2081+
$ret = $container->getEnv('X_FOO');
2082+
unset($_ENV['X_FOO'], $_SERVER['X_FOO']);
2083+
2084+
$this->assertEquals('foo', $ret);
2085+
}
2086+
2087+
public function testGetEnvReturnsStringFromGlobalEnvBeforeProcessEnvIfNotSetInMap(): void
2088+
{
2089+
$container = new Container([]);
2090+
2091+
$_ENV['X_FOO'] = 'foo';
2092+
putenv('X_FOO=bar');
2093+
$ret = $container->getEnv('X_FOO');
2094+
unset($_ENV['X_FOO']);
2095+
putenv('X_FOO');
2096+
2097+
$this->assertEquals('foo', $ret);
2098+
}
2099+
20532100
public function testGetEnvReturnsStringFromPsrContainer(): void
20542101
{
20552102
$psr = $this->createMock(ContainerInterface::class);
@@ -2074,10 +2121,42 @@ public function testGetEnvReturnsNullIfPsrContainerHasNoEntry(): void
20742121
$this->assertNull($container->getEnv('X_FOO'));
20752122
}
20762123

2124+
public function testGetEnvReturnsStringFromProcessEnvIfPsrContainerHasNoEntry(): void
2125+
{
2126+
$psr = $this->createMock(ContainerInterface::class);
2127+
$psr->expects($this->atLeastOnce())->method('has')->with('X_FOO')->willReturn(false);
2128+
$psr->expects($this->never())->method('get');
2129+
2130+
assert($psr instanceof ContainerInterface);
2131+
$container = new Container($psr);
2132+
2133+
putenv('X_FOO=bar');
2134+
$ret = $container->getEnv('X_FOO');
2135+
putenv('X_FOO');
2136+
2137+
$this->assertEquals('bar', $ret);
2138+
}
2139+
2140+
public function testGetEnvReturnsStringFromGlobalEnvIfPsrContainerHasNoEntry(): void
2141+
{
2142+
$psr = $this->createMock(ContainerInterface::class);
2143+
$psr->expects($this->atLeastOnce())->method('has')->with('X_FOO')->willReturn(false);
2144+
$psr->expects($this->never())->method('get');
2145+
2146+
assert($psr instanceof ContainerInterface);
2147+
$container = new Container($psr);
2148+
2149+
$_ENV['X_FOO'] = 'bar';
2150+
$ret = $container->getEnv('X_FOO');
2151+
unset($_ENV['X_FOO']);
2152+
2153+
$this->assertEquals('bar', $ret);
2154+
}
2155+
20772156
public function testGetEnvReturnsStringFromGlobalServerIfPsrContainerHasNoEntry(): void
20782157
{
20792158
$psr = $this->createMock(ContainerInterface::class);
2080-
$psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(false);
2159+
$psr->expects($this->atLeastOnce())->method('has')->with('X_FOO')->willReturn(false);
20812160
$psr->expects($this->never())->method('get');
20822161

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

2172+
public function testGetEnvReturnsStringFromGlobalEnvBeforeServerIfPsrContainerHasNoEntry(): void
2173+
{
2174+
$psr = $this->createMock(ContainerInterface::class);
2175+
$psr->expects($this->atLeastOnce())->method('has')->with('X_FOO')->willReturn(false);
2176+
$psr->expects($this->never())->method('get');
2177+
2178+
assert($psr instanceof ContainerInterface);
2179+
$container = new Container($psr);
2180+
2181+
$_ENV['X_FOO'] = 'foo';
2182+
$_SERVER['X_FOO'] = 'bar';
2183+
$ret = $container->getEnv('X_FOO');
2184+
unset($_ENV['X_FOO'], $_SERVER['X_FOO']);
2185+
2186+
$this->assertEquals('foo', $ret);
2187+
}
2188+
2189+
public function testGetEnvReturnsStringFromGlobalEnvBeforeProcessEnvIfPsrContainerHasNoEntry(): void
2190+
{
2191+
$psr = $this->createMock(ContainerInterface::class);
2192+
$psr->expects($this->atLeastOnce())->method('has')->with('X_FOO')->willReturn(false);
2193+
$psr->expects($this->never())->method('get');
2194+
2195+
assert($psr instanceof ContainerInterface);
2196+
$container = new Container($psr);
2197+
2198+
$_ENV['X_FOO'] = 'foo';
2199+
putenv('X_FOO=bar');
2200+
$ret = $container->getEnv('X_FOO');
2201+
unset($_ENV['X_FOO']);
2202+
putenv('X_FOO');
2203+
2204+
$this->assertEquals('foo', $ret);
2205+
}
2206+
20932207
public function testGetEnvThrowsIfMapContainsInvalidType(): void
20942208
{
20952209
$container = new Container([

0 commit comments

Comments
 (0)