Skip to content

Commit ba8a7ea

Browse files
committed
Support loading environment variables from Container
1 parent 4dd692d commit ba8a7ea

6 files changed

Lines changed: 197 additions & 48 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,26 @@ all uppercase in any factory function like this:
402402
// …
403403
```
404404

405+
=== "Built-in environment variables"
406+
407+
```php title="public/index.php"
408+
<?php
409+
410+
require __DIR__ . '/../vendor/autoload.php';
411+
412+
$container = new FrameworkX\Container([
413+
// Framework X also uses environment variables internally.
414+
// You may explicitly configure this built-in functionality like this:
415+
// 'X_LISTEN' => '0.0.0.0:8081'
416+
// 'X_LISTEN' => fn(?string $PORT = '8080') => '0.0.0.0:' . $PORT
417+
'X_LISTEN' => '127.0.0.1:8080'
418+
]);
419+
420+
$app = new FrameworkX\App($container);
421+
422+
// …
423+
```
424+
405425
> ℹ️ **Passing environment variables**
406426
>
407427
> All environment variables defined on the process level will be made available

docs/best-practices/deployment.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,15 @@ or `[::]` IPv6 address like this:
277277
$ X_LISTEN=0.0.0.0:8080 php public/index.php
278278
```
279279

280+
> ℹ️ **Saving environment variables**
281+
>
282+
> For temporary testing purposes, you may explicitly `export` your environment
283+
> variables on the command like above. As a more permanent solution, you may
284+
> want to save your environment variables in your [systemd configuration](#systemd),
285+
> [Docker settings](#docker-containers), load your variables from a dotenv file
286+
> (`.env`) using a library such as [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv),
287+
> or use an explicit [Container configuration](controllers.md#container-configuration).
288+
280289
### Memory limit
281290

282291
X is carefully designed to minimize memory usage. Depending on your application

src/App.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class App
2727
/** @var SapiHandler */
2828
private $sapi;
2929

30+
/** @var Container */
31+
private $container;
32+
3033
/**
3134
* Instantiate new X application
3235
*
@@ -46,19 +49,19 @@ public function __construct(...$middleware)
4649
// new MiddlewareHandler([$fiberHandler, $accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
4750
$handlers = [];
4851

49-
$container = $needsErrorHandler = new Container();
52+
$this->container = $needsErrorHandler = new Container();
5053

5154
// only log for built-in webserver and PHP development webserver by default, others have their own access log
52-
$needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') ? $container : null;
55+
$needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') ? $this->container : null;
5356

5457
if ($middleware) {
5558
$needsErrorHandlerNext = false;
5659
foreach ($middleware as $handler) {
5760
// load AccessLogHandler and ErrorHandler instance from last Container
5861
if ($handler === AccessLogHandler::class) {
59-
$handler = $container->getAccessLogHandler();
62+
$handler = $this->container->getAccessLogHandler();
6063
} elseif ($handler === ErrorHandler::class) {
61-
$handler = $container->getErrorHandler();
64+
$handler = $this->container->getErrorHandler();
6265
}
6366

6467
// ensure AccessLogHandler is always followed by ErrorHandler
@@ -69,14 +72,14 @@ public function __construct(...$middleware)
6972

7073
if ($handler instanceof Container) {
7174
// remember last Container to load any following class names
72-
$container = $handler;
75+
$this->container = $handler;
7376

7477
// add default ErrorHandler from last Container before adding any other handlers, may be followed by other Container instances (unlikely)
7578
if (!$handlers) {
76-
$needsErrorHandler = $needsAccessLog = $container;
79+
$needsErrorHandler = $needsAccessLog = $this->container;
7780
}
7881
} elseif (!\is_callable($handler)) {
79-
$handlers[] = $container->callable($handler);
82+
$handlers[] = $this->container->callable($handler);
8083
} else {
8184
// don't need a default ErrorHandler if we're adding one as first handler or AccessLogHandler as first followed by one
8285
if ($needsErrorHandler && ($handler instanceof ErrorHandler || $handler instanceof AccessLogHandler) && !$handlers) {
@@ -109,7 +112,7 @@ public function __construct(...$middleware)
109112
\array_unshift($handlers, new FiberHandler()); // @codeCoverageIgnore
110113
}
111114

112-
$this->router = new RouteHandler($container);
115+
$this->router = new RouteHandler($this->container);
113116
$handlers[] = $this->router;
114117
$this->handler = new MiddlewareHandler($handlers);
115118
$this->sapi = new SapiHandler();
@@ -232,7 +235,7 @@ private function runLoop()
232235
return $this->handleRequest($request);
233236
});
234237

235-
$listen = $_SERVER['X_LISTEN'] ?? '127.0.0.1:8080';
238+
$listen = $this->container->getEnv('X_LISTEN') ?? '127.0.0.1:8080';
236239

237240
$socket = new SocketServer($listen);
238241
$http->listen($socket);

src/Container.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ public function callable(string $class): callable
9191
};
9292
}
9393

94+
/** @internal */
95+
public function getEnv(string $name): ?string
96+
{
97+
assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1);
98+
99+
if (\is_array($this->container) && \array_key_exists($name, $this->container)) {
100+
$value = $this->loadVariable($name, 'mixed', true, 64);
101+
} elseif ($this->container instanceof ContainerInterface && $this->container->has($name)) {
102+
$value = $this->container->get($name);
103+
} else {
104+
$value = $_SERVER[$name] ?? null;
105+
}
106+
107+
if (!\is_string($value) && $value !== null) {
108+
throw new \TypeError('Environment variable $' . $name . ' expected type string|null, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
109+
}
110+
111+
return $value;
112+
}
113+
94114
/** @internal */
95115
public function getAccessLogHandler(): AccessLogHandler
96116
{

tests/AppTest.php

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,6 @@
3838

3939
class AppTest extends TestCase
4040
{
41-
/**
42-
* @var array
43-
*/
44-
private $serverArgs;
45-
46-
protected function setUp(): void
47-
{
48-
// Store a snapshot of $_SERVER
49-
$this->serverArgs = $_SERVER;
50-
}
51-
52-
protected function tearDown(): void
53-
{
54-
// Restore $_SERVER as it was before
55-
foreach ($_SERVER as $key => $value) {
56-
if (!\array_key_exists($key, $this->serverArgs)) {
57-
unset($_SERVER[$key]);
58-
continue;
59-
}
60-
$_SERVER[$key] = $value;
61-
}
62-
}
63-
6441
public function testConstructWithMiddlewareAssignsGivenMiddleware()
6542
{
6643
$middleware = function () { };
@@ -626,14 +603,17 @@ public function testRunWillReportListeningAddressAndRunLoopWithSocketServer()
626603
$app->run();
627604
}
628605

629-
public function testRunWillReportListeningAddressFromEnvironmentAndRunLoopWithSocketServer()
606+
public function testRunWillReportListeningAddressFromContainerEnvironmentAndRunLoopWithSocketServer()
630607
{
631608
$socket = @stream_socket_server('127.0.0.1:0');
632609
$addr = stream_socket_get_name($socket, false);
633610
fclose($socket);
634611

635-
$_SERVER['X_LISTEN'] = $addr;
636-
$app = new App();
612+
$container = new Container([
613+
'X_LISTEN' => $addr
614+
]);
615+
616+
$app = new App($container);
637617

638618
// lovely: remove socket server on next tick to terminate loop
639619
Loop::futureTick(function () {
@@ -650,10 +630,13 @@ public function testRunWillReportListeningAddressFromEnvironmentAndRunLoopWithSo
650630
$app->run();
651631
}
652632

653-
public function testRunWillReportListeningAddressFromEnvironmentWithRandomPortAndRunLoopWithSocketServer()
633+
public function testRunWillReportListeningAddressFromContainerEnvironmentWithRandomPortAndRunLoopWithSocketServer()
654634
{
655-
$_SERVER['X_LISTEN'] = '127.0.0.1:0';
656-
$app = new App();
635+
$container = new Container([
636+
'X_LISTEN' => '127.0.0.1:0'
637+
]);
638+
639+
$app = new App($container);
657640

658641
// lovely: remove socket server on next tick to terminate loop
659642
Loop::futureTick(function () {
@@ -672,8 +655,11 @@ public function testRunWillReportListeningAddressFromEnvironmentWithRandomPortAn
672655

673656
public function testRunWillRestartLoopUntilSocketIsClosed()
674657
{
675-
$_SERVER['X_LISTEN'] = '127.0.0.1:0';
676-
$app = new App();
658+
$container = new Container([
659+
'X_LISTEN' => '127.0.0.1:0'
660+
]);
661+
662+
$app = new App($container);
677663

678664
// lovely: remove socket server on next tick to terminate loop
679665
Loop::futureTick(function () {
@@ -700,8 +686,11 @@ public function testRunWillRestartLoopUntilSocketIsClosed()
700686
*/
701687
public function testRunWillStopWhenReceivingSigint()
702688
{
703-
$_SERVER['X_LISTEN'] = '127.0.0.1:0';
704-
$app = new App();
689+
$container = new Container([
690+
'X_LISTEN' => '127.0.0.1:0'
691+
]);
692+
693+
$app = new App($container);
705694

706695
Loop::futureTick(function () {
707696
posix_kill(getmypid(), defined('SIGINT') ? SIGINT : 2);
@@ -717,8 +706,11 @@ public function testRunWillStopWhenReceivingSigint()
717706
*/
718707
public function testRunWillStopWhenReceivingSigterm()
719708
{
720-
$_SERVER['X_LISTEN'] = '127.0.0.1:0';
721-
$app = new App();
709+
$container = new Container([
710+
'X_LISTEN' => '127.0.0.1:0'
711+
]);
712+
713+
$app = new App($container);
722714

723715
Loop::futureTick(function () {
724716
posix_kill(getmypid(), defined('SIGTERM') ? SIGTERM : 15);
@@ -730,8 +722,12 @@ public function testRunWillStopWhenReceivingSigterm()
730722

731723
public function testRunAppWithEmptyAddressThrows()
732724
{
733-
$_SERVER['X_LISTEN'] = '';
734-
$app = new App();
725+
$container = new Container([
726+
'X_LISTEN' => ''
727+
]);
728+
729+
$app = new App($container);
730+
735731

736732
$this->expectException(\InvalidArgumentException::class);
737733
$app->run();
@@ -746,8 +742,11 @@ public function testRunAppWithBusyPortThrows()
746742
$this->markTestSkipped('System does not prevent listening on same address twice');
747743
}
748744

749-
$_SERVER['X_LISTEN'] = $addr;
750-
$app = new App();
745+
$container = new Container([
746+
'X_LISTEN' => $addr
747+
]);
748+
749+
$app = new App($container);
751750

752751
$this->expectException(\RuntimeException::class);
753752
$this->expectExceptionMessage('Failed to listen on');

tests/ContainerTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,6 +1951,104 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidCl
19511951
$callable($request);
19521952
}
19531953

1954+
public function testGetEnvReturnsNullWhenEnvironmentDoesNotExist()
1955+
{
1956+
$container = new Container([]);
1957+
1958+
$this->assertNull($container->getEnv('X_FOO'));
1959+
}
1960+
1961+
public function testGetEnvReturnsStringFromMap()
1962+
{
1963+
$container = new Container([
1964+
'X_FOO' => 'bar'
1965+
]);
1966+
1967+
$this->assertEquals('bar', $container->getEnv('X_FOO'));
1968+
}
1969+
1970+
public function testGetEnvReturnsStringFromMapFactory()
1971+
{
1972+
$container = new Container([
1973+
'X_FOO' => function (string $bar) { return $bar; },
1974+
'bar' => 'bar'
1975+
]);
1976+
1977+
$this->assertEquals('bar', $container->getEnv('X_FOO'));
1978+
}
1979+
1980+
public function testGetEnvReturnsStringFromGlobalServerIfNotSetInMap()
1981+
{
1982+
$container = new Container([]);
1983+
1984+
$_SERVER['X_FOO'] = 'bar';
1985+
$ret = $container->getEnv('X_FOO');
1986+
unset($_SERVER['X_FOO']);
1987+
1988+
$this->assertEquals('bar', $ret);
1989+
}
1990+
1991+
public function testGetEnvReturnsStringFromPsrContainer()
1992+
{
1993+
$psr = $this->createMock(ContainerInterface::class);
1994+
$psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(true);
1995+
$psr->expects($this->once())->method('get')->with('X_FOO')->willReturn('bar');
1996+
1997+
$container = new Container($psr);
1998+
1999+
$this->assertEquals('bar', $container->getEnv('X_FOO'));
2000+
}
2001+
2002+
public function testGetEnvReturnsNullIfPsrContainerHasNoEntry()
2003+
{
2004+
$psr = $this->createMock(ContainerInterface::class);
2005+
$psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(false);
2006+
$psr->expects($this->never())->method('get');
2007+
2008+
$container = new Container($psr);
2009+
2010+
$this->assertNull($container->getEnv('X_FOO'));
2011+
}
2012+
2013+
public function testGetEnvReturnsStringFromGlobalServerIfPsrContainerHasNoEntry()
2014+
{
2015+
$psr = $this->createMock(ContainerInterface::class);
2016+
$psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(false);
2017+
$psr->expects($this->never())->method('get');
2018+
2019+
$container = new Container($psr);
2020+
2021+
$_SERVER['X_FOO'] = 'bar';
2022+
$ret = $container->getEnv('X_FOO');
2023+
unset($_SERVER['X_FOO']);
2024+
2025+
$this->assertEquals('bar', $ret);
2026+
}
2027+
2028+
public function testGetEnvThrowsIfMapContainsInvalidType()
2029+
{
2030+
$container = new Container([
2031+
'X_FOO' => false
2032+
]);
2033+
2034+
$this->expectException(\TypeError::class);
2035+
$this->expectExceptionMessage('Environment variable $X_FOO expected type string|null, but got boolean');
2036+
$container->getEnv('X_FOO');
2037+
}
2038+
2039+
public function testGetEnvThrowsIfMapPsrContainerReturnsInvalidType()
2040+
{
2041+
$psr = $this->createMock(ContainerInterface::class);
2042+
$psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(true);
2043+
$psr->expects($this->once())->method('get')->with('X_FOO')->willReturn(42);
2044+
2045+
$container = new Container($psr);
2046+
2047+
$this->expectException(\TypeError::class);
2048+
$this->expectExceptionMessage('Environment variable $X_FOO expected type string|null, but got integer');
2049+
$container->getEnv('X_FOO');
2050+
}
2051+
19542052
public function testGetAccessLogHandlerReturnsDefaultAccessLogHandlerInstance()
19552053
{
19562054
$container = new Container([]);

0 commit comments

Comments
 (0)