Skip to content
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
33 changes: 19 additions & 14 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,25 +381,30 @@ all uppercase in any factory function like this:
// …
```

=== "Built-in environment variables"
Besides defining custom environment variables, you may also override built-in
environment variables used by X itself like this:

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

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

$container = new FrameworkX\Container([
// Framework X also uses environment variables internally.
// You may explicitly configure this built-in functionality like this:
// 'X_LISTEN' => '0.0.0.0:8081'
// 'X_LISTEN' => fn(int|string $PORT = 8080) => '0.0.0.0:' . $PORT
'X_LISTEN' => fn(string $X_LISTEN = '127.0.0.1:8080') => $X_LISTEN
]);
$container = new FrameworkX\Container([
// Framework X also uses environment variables internally.
// You may explicitly configure this built-in functionality like this:
// 'X_LISTEN' => '0.0.0.0:8081'
// 'X_LISTEN' => fn(int|string $PORT = 8080) => '0.0.0.0:' . $PORT
'X_LISTEN' => fn(string $X_LISTEN = '127.0.0.1:8080') => $X_LISTEN,

// 'X_EXPERIMENTAL_RUNNER' => AcmeRunner::class
// 'X_EXPERIMENTAL_RUNNER' => fn(bool|string $ACME = false): ?string => $ACME ? AcmeRunner::class : null
'X_EXPERIMENTAL_RUNNER' => fn(?string $X_EXPERIMENTAL_RUNNER = null): ?string => $X_EXPERIMENTAL_RUNNER,
]);

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

// …
```
// …
```

> ℹ️ **Passing environment variables**
>
Expand Down
7 changes: 6 additions & 1 deletion src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class App
/** @var RouteHandler */
private $router;

/** @var HttpServerRunner|SapiRunner */
/** @var HttpServerRunner|SapiRunner|callable(callable(ServerRequestInterface):(ResponseInterface|PromiseInterface<ResponseInterface>)):void */
private $runner;

/**
Expand Down Expand Up @@ -253,8 +253,13 @@ public function redirect(string $route, string $target, int $code = Response::ST
* This is particularly useful because it allows you to run the exact same
* application code in any environment.
*
* For more advanced use cases, this behavior can be overridden by setting
* the `X_EXPERIMENTAL_RUNNER` environment variable to the desired runner
* class name ({@see Container::getRunner()}).
*
* @see HttpServerRunner::__invoke()
* @see SapiRunner::__invoke()
* @see Container::getRunner()
*/
public function run(): void
{
Expand Down
24 changes: 21 additions & 3 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,32 @@ public function getObject(string $class) /*: object (PHP 7.2+) */
/**
* [Internal] Get the app runner appropriate for this environment from container
*
* @return HttpServerRunner|SapiRunner
* By default, this method returns an instance of `HttpServerRunner` when
* running in CLI mode, and an instance of `SapiRunner` when running in a
* traditional web server environment.
*
* For more advanced use cases, this behavior can be overridden by setting
* the `X_EXPERIMENTAL_RUNNER` environment variable to the desired runner
* class name. The specified class must be invokable with the main request
* handler signature. Note that this is an experimental feature and the API
* may be subject to change in future releases.
*
* @return HttpServerRunner|SapiRunner|callable(callable(ServerRequestInterface):(\Psr\Http\Message\ResponseInterface|\React\Promise\PromiseInterface<\Psr\Http\Message\ResponseInterface>)):void
* @throws \TypeError if container config or factory returns an unexpected type
* @throws \Throwable if container factory function throws unexpected exception
* @internal
* @see App::run()
*/
public function getRunner() /*: HttpServerRunner|SapiRunner (PHP 8.0+) */
public function getRunner(): callable /*: HttpServerRunner|SapiRunner|callable (PHP 8.0+) */
{
return $this->getObject(\PHP_SAPI === 'cli' ? HttpServerRunner::class : SapiRunner::class);
// @phpstan-ignore-next-line `getObject()` already performs type checks if `getEnv()` returns an invalid class
$runner = $this->getObject($this->getEnv('X_EXPERIMENTAL_RUNNER') ?? (\PHP_SAPI === 'cli' ? HttpServerRunner::class : SapiRunner::class));
if (!\is_callable($runner)) {
throw new \TypeError(
'Return value of ' . __METHOD__ . '() must be of type callable, ' . $this->gettype($runner) . ' returned'
);
}
return $runner;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,22 @@ public function testRunWillInvokeRunnerFromContainer(): void
$app->run();
}

public function testRunWillInvokeCustomRunnerFromContainerEnvironmentVariable(): void
{
$runner = $this->createMock(HttpServerRunner::class);
$runner->expects($this->once())->method('__invoke');

$container = new Container([
'X_EXPERIMENTAL_RUNNER' => get_class($runner),
get_class($runner) => $runner,
HttpServerRunner::class => function () { throw new \BadFunctionCallException('Should not be called'); }
]);

$app = new App($container);

$app->run();
}

public function testGetMethodAddsGetRouteOnRouter(): void
{
$router = $this->createMock(RouteHandler::class);
Expand Down
64 changes: 61 additions & 3 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,7 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstance(): void

$this->assertInstanceOf(HttpServerRunner::class, $runner);

assert($runner instanceof HttpServerRunner);
$ref = new \ReflectionProperty($runner, 'listenAddress');
if (PHP_VERSION_ID < 80100) {
$ref->setAccessible(true);
Expand All @@ -2852,6 +2853,7 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithCustomLis

$this->assertInstanceOf(HttpServerRunner::class, $runner);

assert($runner instanceof HttpServerRunner);
$ref = new \ReflectionProperty($runner, 'listenAddress');
if (PHP_VERSION_ID < 80100) {
$ref->setAccessible(true);
Expand Down Expand Up @@ -2888,7 +2890,10 @@ public function testGetRunnerReturnsHttpServerRunnerInstanceFromPsrContainer():
$runner = new HttpServerRunner(new LogStreamHandler('php://output'), null);

$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->once())->method('has')->with(HttpServerRunner::class)->willReturn(true);
$psr->expects($this->exactly(2))->method('has')->willReturnMap([
['X_EXPERIMENTAL_RUNNER', false],
[HttpServerRunner::class, true],
]);
$psr->expects($this->once())->method('get')->with(HttpServerRunner::class)->willReturn($runner);

assert($psr instanceof ContainerInterface);
Expand All @@ -2902,7 +2907,8 @@ public function testGetRunnerReturnsHttpServerRunnerInstanceFromPsrContainer():
public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithDefaultListenAddressIfPsrContainerHasNoEntry(): void
{
$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->exactly(2))->method('has')->willReturnMap([
$psr->expects($this->exactly(3))->method('has')->willReturnMap([
['X_EXPERIMENTAL_RUNNER', false],
[HttpServerRunner::class, false],
['X_LISTEN', false],
]);
Expand All @@ -2915,6 +2921,7 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithDefaultLi

$this->assertInstanceOf(HttpServerRunner::class, $runner);

assert($runner instanceof HttpServerRunner);
$ref = new \ReflectionProperty($runner, 'listenAddress');
if (PHP_VERSION_ID < 80100) {
$ref->setAccessible(true);
Expand All @@ -2927,7 +2934,8 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithDefaultLi
public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithCustomListenAddressIfPsrContainerHasNoEntryButCustomListenAddress(): void
{
$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->exactly(2))->method('has')->willReturnMap([
$psr->expects($this->exactly(3))->method('has')->willReturnMap([
['X_EXPERIMENTAL_RUNNER', false],
[HttpServerRunner::class, false],
['X_LISTEN', true],
]);
Expand All @@ -2940,6 +2948,7 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithCustomLis

$this->assertInstanceOf(HttpServerRunner::class, $runner);

assert($runner instanceof HttpServerRunner);
$ref = new \ReflectionProperty($runner, 'listenAddress');
if (PHP_VERSION_ID < 80100) {
$ref->setAccessible(true);
Expand All @@ -2949,6 +2958,55 @@ public function testGetRunnerReturnsDefaultHttpServerRunnerInstanceWithCustomLis
$this->assertEquals('127.0.0.1:8081', $listenAddress);
}

public function testGetRunnerReturnsCustomRunnerInstanceFromEnvironmentVariable(): void
{
$runner = new class {
public function __invoke(): void {}
};

$container = new Container([
'X_EXPERIMENTAL_RUNNER' => get_class($runner),
get_class($runner) => $runner
]);

$ret = $container->getRunner();

$this->assertSame($runner, $ret);
}

public function testGetRunnerThrowsForInvalidEnvironmentVariableType(): void
{
$container = new Container([
'X_EXPERIMENTAL_RUNNER' => 42
]);

$this->expectException(\TypeError::class);
$this->expectExceptionMessage('Return value of ' . Container::class . '::getEnv() for $X_EXPERIMENTAL_RUNNER must be of type string|null, int returned');
$container->getRunner();
}

public function testGetRunnerThrowsForUnknownClassNameInEnvironmentVariable(): void
{
$container = new Container([
'X_EXPERIMENTAL_RUNNER' => 'UnknownClass'
]);

$this->expectException(\Error::class);
$this->expectExceptionMessage('Class UnknownClass not found');
$container->getRunner();
}

public function testGetRunnerThrowsForClassNotCallableInEnvironmentVariable(): void
{
$container = new Container([
'X_EXPERIMENTAL_RUNNER' => \stdClass::class
]);

$this->expectException(\TypeError::class);
$this->expectExceptionMessage('Return value of ' . Container::class . '::getRunner() must be of type callable, stdClass returned');
$container->getRunner();
}

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