diff --git a/src/Bref.php b/src/Bref.php index 180cdbac6..06d93a52c 100644 --- a/src/Bref.php +++ b/src/Bref.php @@ -2,6 +2,7 @@ namespace Bref; +use Bref\Listener\EventDispatcher; use Bref\Runtime\FileHandlerLocator; use Closure; use Psr\Container\ContainerInterface; @@ -11,10 +12,14 @@ class Bref { private static ?Closure $containerProvider = null; private static ?ContainerInterface $container = null; + /** + * TODO deprecate hooks when the event dispatcher is stable. + */ private static array $hooks = [ 'beforeStartup' => [], 'beforeInvoke' => [], ]; + private static EventDispatcher $eventDispatcher; /** * Configure the container that provides Lambda handlers. @@ -26,6 +31,17 @@ public static function setContainer(Closure $containerProvider): void self::$containerProvider = $containerProvider; } + /** + * @internal This API is experimental and may change at any time. + */ + public static function events(): EventDispatcher + { + if (! isset(self::$eventDispatcher)) { + self::$eventDispatcher = new EventDispatcher; + } + return self::$eventDispatcher; + } + /** * Register a hook to be executed before the runtime starts. * diff --git a/src/ConsoleRuntime/Main.php b/src/ConsoleRuntime/Main.php index b5c3bbeeb..44359c0ee 100755 --- a/src/ConsoleRuntime/Main.php +++ b/src/ConsoleRuntime/Main.php @@ -19,6 +19,7 @@ public static function run(): void LazySecretsLoader::loadSecretEnvironmentVariables(); Bref::triggerHooks('beforeStartup'); + Bref::events()->beforeStartup(); $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('console'); @@ -28,6 +29,8 @@ public static function run(): void $lambdaRuntime->failInitialization("Handler `$handlerFile` doesn't exist", 'Runtime.NoSuchHandler'); } + Bref::events()->afterStartup(); + /** @phpstan-ignore-next-line */ while (true) { $lambdaRuntime->processNextEvent(function ($event, Context $context) use ($handlerFile): array { diff --git a/src/FpmRuntime/Main.php b/src/FpmRuntime/Main.php index d3241b5d1..fa462c113 100755 --- a/src/FpmRuntime/Main.php +++ b/src/FpmRuntime/Main.php @@ -22,6 +22,7 @@ public static function run(): void LazySecretsLoader::loadSecretEnvironmentVariables(); Bref::triggerHooks('beforeStartup'); + Bref::events()->beforeStartup(); $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('fpm'); @@ -38,6 +39,8 @@ public static function run(): void $lambdaRuntime->failInitialization(new RuntimeException('Error while starting PHP-FPM: ' . $e->getMessage(), 0, $e)); } + Bref::events()->afterStartup(); + /** @phpstan-ignore-next-line */ while (true) { $lambdaRuntime->processNextEvent($phpFpm); diff --git a/src/FunctionRuntime/Main.php b/src/FunctionRuntime/Main.php index f6925ca06..8d335ce8b 100644 --- a/src/FunctionRuntime/Main.php +++ b/src/FunctionRuntime/Main.php @@ -17,6 +17,7 @@ public static function run(): void LazySecretsLoader::loadSecretEnvironmentVariables(); Bref::triggerHooks('beforeStartup'); + Bref::events()->beforeStartup(); $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('function'); @@ -28,6 +29,8 @@ public static function run(): void $lambdaRuntime->failInitialization($e, 'Runtime.NoSuchHandler'); } + Bref::events()->afterStartup(); + $loopMax = getenv('BREF_LOOP_MAX') ?: 1; $loops = 0; while (true) { diff --git a/src/Listener/BrefEventSubscriber.php b/src/Listener/BrefEventSubscriber.php new file mode 100644 index 000000000..489c3cfbe --- /dev/null +++ b/src/Listener/BrefEventSubscriber.php @@ -0,0 +1,59 @@ +subscribers[] = $subscriber; + } + + /** + * Trigger the `beforeStartup` event. + * + * @internal This method is called by Bref and should not be called by user code. + */ + public function beforeStartup(): void + { + foreach ($this->subscribers as $listener) { + $listener->beforeStartup(); + } + } + + /** + * Trigger the `afterStartup` event. + * + * @internal This method is called by Bref and should not be called by user code. + */ + public function afterStartup(): void + { + foreach ($this->subscribers as $listener) { + $listener->afterStartup(); + } + } + + /** + * Trigger the `beforeInvoke` event. + * + * @internal This method is called by Bref and should not be called by user code. + */ + public function beforeInvoke( + callable | Handler | RequestHandlerInterface $handler, + mixed $event, + Context $context, + ): void { + foreach ($this->subscribers as $listener) { + $listener->beforeInvoke($handler, $event, $context); + } + } + + /** + * Trigger the `afterInvoke` event. + * + * @internal This method is called by Bref and should not be called by user code. + */ + public function afterInvoke( + callable | Handler | RequestHandlerInterface $handler, + mixed $event, + Context $context, + mixed $result, + \Throwable | null $error = null, + ): void { + foreach ($this->subscribers as $listener) { + $listener->afterInvoke($handler, $event, $context, $result, $error); + } + } +} diff --git a/src/Runtime/LambdaRuntime.php b/src/Runtime/LambdaRuntime.php index 0b4014c4c..3e311bd6d 100755 --- a/src/Runtime/LambdaRuntime.php +++ b/src/Runtime/LambdaRuntime.php @@ -82,6 +82,7 @@ public function processNextEvent(Handler | RequestHandlerInterface | callable $h [$event, $context] = $this->waitNextInvocation(); Bref::triggerHooks('beforeInvoke'); + Bref::events()->beforeInvoke($handler, $event, $context); $this->ping(); @@ -89,9 +90,13 @@ public function processNextEvent(Handler | RequestHandlerInterface | callable $h $result = $this->invoker->invoke($handler, $event, $context); $this->sendResponse($context->getAwsRequestId(), $result); + + Bref::events()->afterInvoke($handler, $event, $context, $result); } catch (Throwable $e) { $this->signalFailure($context->getAwsRequestId(), $e); + Bref::events()->afterInvoke($handler, $event, $context, null, $e); + return false; } diff --git a/tests/Listener/EventDispatcherTest.php b/tests/Listener/EventDispatcherTest.php new file mode 100644 index 000000000..701753bfb --- /dev/null +++ b/tests/Listener/EventDispatcherTest.php @@ -0,0 +1,31 @@ +subscribe($subscriber); + + $eventDispatcher->beforeStartup(); + $this->assertTrue($subscriber->invokedBeforeStartup); + + $handler = fn () => null; + $event = new stdClass; + $context = Context::fake(); + $eventDispatcher->beforeInvoke($handler, $event, $context); + $this->assertEquals([$handler, $event, $context], $subscriber->invokedBeforeInvoke); + + $result = new stdClass; + $eventDispatcher->afterInvoke($handler, $event, $context, $result); + $this->assertEquals([$handler, $event, $context, $result, null], $subscriber->invokedAfterInvoke); + } +} diff --git a/tests/Listener/FakeSubscriber.php b/tests/Listener/FakeSubscriber.php new file mode 100644 index 000000000..446db1126 --- /dev/null +++ b/tests/Listener/FakeSubscriber.php @@ -0,0 +1,33 @@ +invokedBeforeStartup = true; + } + + /** + * @param mixed ...$params + */ + public function beforeInvoke(...$params): void + { + $this->invokedBeforeInvoke = $params; + } + + /** + * @param mixed ...$params + */ + public function afterInvoke(...$params): void + { + $this->invokedAfterInvoke = $params; + } +}