Skip to content

Commit 857b354

Browse files
authored
Merge pull request #184 from clue-labs/env
Support loading environment variables from DI container configuration
2 parents 214ce42 + ba8a7ea commit 857b354

File tree

6 files changed

+479
-52
lines changed

6 files changed

+479
-52
lines changed

docs/best-practices/controllers.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,80 @@ some manual configuration like this:
358358
> namespaced class names like in the previous example. You may also want to make
359359
> sure that container variables use unique names prefixed with your vendor name.
360360
361+
All environment variables will be made available as container variables
362+
automatically. You can access their values simply by referencing variables in
363+
all uppercase in any factory function like this:
364+
365+
=== "Required environment variables"
366+
367+
```php title="public/index.php"
368+
<?php
369+
370+
require __DIR__ . '/../vendor/autoload.php';
371+
372+
$container = new FrameworkX\Container([
373+
React\MySQL\ConnectionInterface::class => function (string $MYSQL_URI) {
374+
// connect to database defined in required $MYSQL_URI environment variable
375+
return (new React\MySQL\Factory())->createLazyConnection($MYSQL_URI);
376+
}
377+
]);
378+
379+
380+
$app = new FrameworkX\App($container);
381+
382+
// …
383+
```
384+
385+
=== "Optional environment variables"
386+
387+
```php title="public/index.php"
388+
<?php
389+
390+
require __DIR__ . '/../vendor/autoload.php';
391+
392+
$container = new FrameworkX\Container([
393+
React\MySQL\ConnectionInterface::class => function (string $DB_HOST = 'localhost', string $DB_USER = 'root', string $DB_PASS = '', string $DB_NAME = 'acme') {
394+
// connect to database defined in optional $DB_* environment variables
395+
$uri = 'mysql://' . $DB_USER . ':' . rawurlencode($DB_PASS) . '@' . $DB_HOST . '/' . $DB_NAME . '?idle=0.001';
396+
return (new React\MySQL\Factory())->createLazyConnection($uri);
397+
}
398+
]);
399+
400+
$app = new FrameworkX\App($container);
401+
402+
// …
403+
```
404+
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+
425+
> ℹ️ **Passing environment variables**
426+
>
427+
> All environment variables defined on the process level will be made available
428+
> automatically. For temporary testing purposes, you may explicitly `export` or
429+
> prefix environment variables to the command line. As a more permanent
430+
> solution, you may want to save your environment variables in your
431+
> [systemd configuration](deployment.md#systemd), [Docker settings](deployment.md#docker-containers),
432+
> or load your variables from a dotenv file (`.env`) using a library such as
433+
> [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv).
434+
361435
The container configuration may also be used to map a class name to a different
362436
class name that implements the same interface, either by mapping between two
363437
class names or using a factory function that returns a class name. This is

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: 29 additions & 4 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
{
@@ -222,7 +242,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
222242

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

@@ -264,8 +284,9 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
264284
*/
265285
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
266286
{
267-
assert(\array_key_exists($name, $this->container));
268-
if ($this->container[$name] instanceof \Closure) {
287+
assert(\array_key_exists($name, $this->container) || isset($_SERVER[$name]));
288+
289+
if (($this->container[$name] ?? null) instanceof \Closure) {
269290
if ($depth < 1) {
270291
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
271292
}
@@ -282,9 +303,13 @@ private function loadVariable(string $name, string $type, bool $nullable, int $d
282303
}
283304

284305
$this->container[$name] = $value;
306+
} elseif (\array_key_exists($name, $this->container)) {
307+
$value = $this->container[$name];
308+
} else {
309+
assert(isset($_SERVER[$name]) && \is_string($_SERVER[$name]));
310+
$value = $_SERVER[$name];
285311
}
286312

287-
$value = $this->container[$name];
288313
assert(\is_object($value) || \is_scalar($value) || $value === null);
289314

290315
// allow null values if parameter is marked nullable or untyped or mixed

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');

0 commit comments

Comments
 (0)