Skip to content

Commit

Permalink
Automatically load environment variables for DI container factories
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Aug 2, 2022
1 parent 214ce42 commit 4dd692d
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 4 deletions.
54 changes: 54 additions & 0 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,60 @@ some manual configuration like this:
> namespaced class names like in the previous example. You may also want to make
> sure that container variables use unique names prefixed with your vendor name.
All environment variables will be made available as container variables
automatically. You can access their values simply by referencing variables in
all uppercase in any factory function like this:

=== "Required environment variables"

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
React\MySQL\ConnectionInterface::class => function (string $MYSQL_URI) {
// connect to database defined in required $MYSQL_URI environment variable
return (new React\MySQL\Factory())->createLazyConnection($MYSQL_URI);
}
]);


$app = new FrameworkX\App($container);

// …
```

=== "Optional environment variables"

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
React\MySQL\ConnectionInterface::class => function (string $DB_HOST = 'localhost', string $DB_USER = 'root', string $DB_PASS = '', string $DB_NAME = 'acme') {
// connect to database defined in optional $DB_* environment variables
$uri = 'mysql://' . $DB_USER . ':' . rawurlencode($DB_PASS) . '@' . $DB_HOST . '/' . $DB_NAME . '?idle=0.001';
return (new React\MySQL\Factory())->createLazyConnection($uri);
}
]);

$app = new FrameworkX\App($container);

// …
```

> ℹ️ **Passing environment variables**
>
> All environment variables defined on the process level will be made available
> automatically. For temporary testing purposes, you may explicitly `export` or
> prefix environment variables to the command line. As a more permanent
> solution, you may want to save your environment variables in your
> [systemd configuration](deployment.md#systemd), [Docker settings](deployment.md#docker-containers),
> or load your variables from a dotenv file (`.env`) using a library such as
> [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv).
The container configuration may also be used to map a class name to a different
class name that implements the same interface, either by mapping between two
class names or using a factory function that returns a class name. This is
Expand Down
13 changes: 9 additions & 4 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,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)) {
if ($allowVariables && (\array_key_exists($parameter->getName(), $this->container) || (isset($_SERVER[$parameter->getName()]) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $parameter->getName())))) {
return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth);
}

Expand Down Expand Up @@ -264,8 +264,9 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
*/
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
{
assert(\array_key_exists($name, $this->container));
if ($this->container[$name] instanceof \Closure) {
assert(\array_key_exists($name, $this->container) || isset($_SERVER[$name]));

if (($this->container[$name] ?? null) instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
}
Expand All @@ -282,9 +283,13 @@ private function loadVariable(string $name, string $type, bool $nullable, int $d
}

$this->container[$name] = $value;
} elseif (\array_key_exists($name, $this->container)) {
$value = $this->container[$name];
} else {
assert(isset($_SERVER[$name]) && \is_string($_SERVER[$name]));
$value = $_SERVER[$name];
}

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

// allow null values if parameter is marked nullable or untyped or mixed
Expand Down
219 changes: 219 additions & 0 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,225 @@ public function __invoke(ServerRequestInterface $request)
$this->assertEquals('{"name":"ADMIN"}', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresStringEnvironmentVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (string $FOO) {
return new Response(200, [], json_encode($FOO));
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$_SERVER['FOO'] = 'bar';
$response = $callable($request);
unset($_SERVER['FOO']);

$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('"bar"', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresStringMappedFromFactoryThatRequiresStringEnvironmentVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (string $address) {
return new Response(200, [], json_encode($address));
},
'address' => function (string $FOO) {
return 'http://' . $FOO;
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$_SERVER['FOO'] = 'bar';
$response = $callable($request);
unset($_SERVER['FOO']);

$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('"http:\/\/bar"', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresNullableStringEnvironmentVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (?string $FOO) {
return new Response(200, [], json_encode($FOO));
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$_SERVER['FOO'] = 'bar';
$response = $callable($request);
unset($_SERVER['FOO']);

$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('"bar"', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresNullableStringEnvironmentVariableAssignsNull()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (?string $FOO) {
return new Response(200, [], json_encode($FOO));
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$response = $callable($request);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('null', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresUntypedEnvironmentVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function ($FOO) {
return new Response(200, [], json_encode($FOO));
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$_SERVER['FOO'] = 'bar';
$response = $callable($request);
unset($_SERVER['FOO']);

$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('"bar"', (string) $response->getBody());
}

/**
* @requires PHP 8
*/
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresMixedEnvironmentVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (mixed $FOO) {
return new Response(200, [], json_encode($FOO));
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

$_SERVER['FOO'] = 'bar';
$response = $callable($request);
unset($_SERVER['FOO']);

$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('"bar"', (string) $response->getBody());
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesUnknownVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down

0 comments on commit 4dd692d

Please sign in to comment.