diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 7e81cef..6e2581c 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -5,4 +5,8 @@ parameters:
- src/
- tests/
+ fileExtensions:
+ - php
+ - phpt
+
reportUnmatchedIgnoredErrors: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index fb514a6..acf5e2b 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,7 +11,8 @@
./tests/
- ./tests/integration/
+ ./tests/
+ ./tests/integration/vendor/
diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy
index a35d140..8113110 100644
--- a/phpunit.xml.legacy
+++ b/phpunit.xml.legacy
@@ -9,7 +9,8 @@
./tests/
- ./tests/install-as-dep/
+ ./tests/
+ ./tests/integration/vendor/
diff --git a/src/App.php b/src/App.php
index 808e3d9..519e157 100644
--- a/src/App.php
+++ b/src/App.php
@@ -5,8 +5,6 @@
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\Io\RedirectHandler;
use FrameworkX\Io\RouteHandler;
-use FrameworkX\Runner\HttpServerRunner;
-use FrameworkX\Runner\SapiRunner;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
@@ -22,7 +20,7 @@ class App
/** @var RouteHandler */
private $router;
- /** @var HttpServerRunner|SapiRunner|callable(callable(ServerRequestInterface):(ResponseInterface|PromiseInterface)):void */
+ /** @var callable(callable(ServerRequestInterface):(ResponseInterface|PromiseInterface)):void */
private $runner;
/**
@@ -257,8 +255,8 @@ public function redirect(string $route, string $target, int $code = Response::ST
* the `X_EXPERIMENTAL_RUNNER` environment variable to the desired runner
* class name ({@see Container::getRunner()}).
*
- * @see HttpServerRunner::__invoke()
- * @see SapiRunner::__invoke()
+ * @see \FrameworkX\Runner\HttpServerRunner::__invoke()
+ * @see \FrameworkX\Runner\SapiRunner::__invoke()
* @see Container::getRunner()
*/
public function run(): void
diff --git a/src/Runner/NullRunner.php b/src/Runner/NullRunner.php
new file mode 100644
index 0000000..8fc5e17
--- /dev/null
+++ b/src/Runner/NullRunner.php
@@ -0,0 +1,41 @@
+ fn(?string $X_EXPERIMENTAL_RUNNER = null): ?string => $X_EXPERIMENTAL_RUNNER,
+ * // 'X_EXPERIMENTAL_RUNNER' => fn(bool|string $ACME = false): ?string => $ACME ? NullRunner::class : null,
+ * 'X_EXPERIMENTAL_RUNNER' => NullRunner::class
+ * ]);
+ *
+ * $app = new App($container);
+ * ```
+ *
+ * Likewise, you may pass this runner through an environment variable from your
+ * integration tests, see also included PHPT test files for examples.
+ *
+ * @see \FrameworkX\Container::getRunner()
+ */
+class NullRunner
+{
+ /**
+ * @param callable(\Psr\Http\Message\ServerRequestInterface):(\Psr\Http\Message\ResponseInterface|\React\Promise\PromiseInterface<\Psr\Http\Message\ResponseInterface>) $handler
+ * @return void
+ */
+ public function __invoke(callable $handler): void
+ {
+ // NO-OP
+ }
+}
diff --git a/tests/AppTest.php b/tests/AppTest.php
index 2b8eb52..fee95df 100644
--- a/tests/AppTest.php
+++ b/tests/AppTest.php
@@ -9,6 +9,7 @@
use FrameworkX\Io\MiddlewareHandler;
use FrameworkX\Io\RouteHandler;
use FrameworkX\Runner\HttpServerRunner;
+use FrameworkX\Runner\NullRunner;
use FrameworkX\Tests\Fixtures\InvalidAbstract;
use FrameworkX\Tests\Fixtures\InvalidConstructorInt;
use FrameworkX\Tests\Fixtures\InvalidConstructorIntersection;
@@ -949,6 +950,18 @@ public function testRunWillInvokeCustomRunnerFromContainerEnvironmentVariable():
$app->run();
}
+ public function testRunReturnsImmediatelyWithNullRunnerFromContainerEnvironmentVariable(): void
+ {
+ $container = new Container([
+ 'X_EXPERIMENTAL_RUNNER' => NullRunner::class
+ ]);
+
+ $app = new App($container);
+
+ $this->expectOutputString('');
+ $app->run();
+ }
+
public function testGetMethodAddsGetRouteOnRouter(): void
{
$router = $this->createMock(RouteHandler::class);
diff --git a/tests/Runner/NullRunnerTest.php b/tests/Runner/NullRunnerTest.php
new file mode 100644
index 0000000..eda32dd
--- /dev/null
+++ b/tests/Runner/NullRunnerTest.php
@@ -0,0 +1,19 @@
+expectOutputString('');
+ $runner(function () {
+ throw new \BadFunctionCallException('Should not be called');
+ });
+ }
+}
diff --git a/tests/integration/public/index.php b/tests/integration/public/index.php
index 8bca0ce..1fceb3a 100644
--- a/tests/integration/public/index.php
+++ b/tests/integration/public/index.php
@@ -26,7 +26,14 @@ function asleep(float $s): PromiseInterface
});
}
-$app = new FrameworkX\App();
+$container = new FrameworkX\Container([
+ FrameworkX\AccessLogHandler::class => function (?string $X_EXPERIMENTAL_RUNNER = null) {
+ // log to /dev/null when running in experimental runner mode to avoid cluttering output
+ return new FrameworkX\AccessLogHandler($X_EXPERIMENTAL_RUNNER !== null ? (DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul') : null);
+ }
+]);
+
+$app = new FrameworkX\App($container);
$app->get('/', function () {
return React\Http\Message\Response::plaintext(
diff --git a/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt
new file mode 100644
index 0000000..4db264f
--- /dev/null
+++ b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Loading index file with NullRunner allows invoking the app
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--ENV--
+X_EXPERIMENTAL_RUNNER=FrameworkX\Runner\NullRunner
+--FILE--
+getBody();
+
+?>
+--EXPECT--
+Hello world!
diff --git a/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt
new file mode 100644
index 0000000..80bf4b7
--- /dev/null
+++ b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt
@@ -0,0 +1,11 @@
+--TEST--
+Loading index file with NullRunner stops immediately without output
+--INI--
+# suppress legacy PHPUnit 7 warning for Xdebug 3
+xdebug.default_enable=
+--ENV--
+X_EXPERIMENTAL_RUNNER=FrameworkX\Runner\NullRunner
+--FILE_EXTERNAL--
+../public/index.php
+--EXPECTREGEX--
+^$