Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework the low-level hook system used by framework integrations #1700

Merged
merged 8 commits into from
Nov 24, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Bref.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Bref;

use Bref\Listener\EventDispatcher;
use Bref\Runtime\FileHandlerLocator;
use Closure;
use Psr\Container\ContainerInterface;
Expand All @@ -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.
Expand All @@ -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.
*
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleRuntime/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static function run(): void
LazySecretsLoader::loadSecretEnvironmentVariables();

Bref::triggerHooks('beforeStartup');
Bref::events()->beforeStartup();

$lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('console');

Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions src/FpmRuntime/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static function run(): void
LazySecretsLoader::loadSecretEnvironmentVariables();

Bref::triggerHooks('beforeStartup');
Bref::events()->beforeStartup();

$lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('fpm');

Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/FunctionRuntime/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static function run(): void
LazySecretsLoader::loadSecretEnvironmentVariables();

Bref::triggerHooks('beforeStartup');
Bref::events()->beforeStartup();

$lambdaRuntime = LambdaRuntime::fromEnvironmentVariable('function');

Expand All @@ -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) {
Expand Down
59 changes: 59 additions & 0 deletions src/Listener/BrefEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);

namespace Bref\Listener;

use Bref\Context\Context;
// @phpcs:disable
use Bref\Event\Handler;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Listen to Bref internal events.
*
* Warning: Bref events are low-level extension points to be used by framework
* integrations. For user code, it is not recommended to use them. Use your
* framework's extension points instead.
*
* @internal This API is experimental and may change at any time.
*/
abstract class BrefEventSubscriber
{
/**
* Register a hook to be executed before the runtime starts.
*/
public function beforeStartup(): void
{
}

/**
* Register a hook to be executed after the runtime has started.
*/
public function afterStartup(): void
{
}

/**
* Register a hook to be executed before any Lambda invocation.
*/
public function beforeInvoke(
callable | Handler | RequestHandlerInterface $handler,
mixed $event,
Context $context,
): void {
}

/**
* Register a hook to be executed after any Lambda invocation.
*
* In case of an error, the `$error` parameter will be set and
* `$result` will be `null`.
*/
public function afterInvoke(
callable | Handler | RequestHandlerInterface $handler,
mixed $event,
Context $context,
mixed $result,
\Throwable | null $error = null,
): void {
}
}
86 changes: 86 additions & 0 deletions src/Listener/EventDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare(strict_types=1);

namespace Bref\Listener;

use Bref\Context\Context;
// @phpcs:disable
use Bref\Event\Handler;
use Psr\Http\Server\RequestHandlerInterface;

/**
* @internal This API is experimental and may change at any time.
*/
final class EventDispatcher extends BrefEventSubscriber
{
/**
* @param BrefEventSubscriber[] $subscribers
*/
public function __construct(
private array $subscribers = [],
) {
}

/**
* Register an event subscriber class.
*/
public function subscribe(BrefEventSubscriber $subscriber): void
{
$this->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);
}
}
}
5 changes: 5 additions & 0 deletions src/Runtime/LambdaRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,21 @@ public function processNextEvent(Handler | RequestHandlerInterface | callable $h
[$event, $context] = $this->waitNextInvocation();

Bref::triggerHooks('beforeInvoke');
Bref::events()->beforeInvoke($handler, $event, $context);

$this->ping();

try {
$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;
}

Expand Down
31 changes: 31 additions & 0 deletions tests/Listener/EventDispatcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace Bref\Test\Listener;

use Bref\Context\Context;
use Bref\Listener\EventDispatcher;
use PHPUnit\Framework\TestCase;
use stdClass;

class EventDispatcherTest extends TestCase
{
public function test subscribe(): void
{
$eventDispatcher = new EventDispatcher;
$subscriber = new FakeSubscriber;
$eventDispatcher->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);
}
}
33 changes: 33 additions & 0 deletions tests/Listener/FakeSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

namespace Bref\Test\Listener;

use Bref\Listener\BrefEventSubscriber;

class FakeSubscriber extends BrefEventSubscriber
{
public bool $invokedBeforeStartup = false;
public mixed $invokedBeforeInvoke = null;
public mixed $invokedAfterInvoke = null;

public function beforeStartup(): void
{
$this->invokedBeforeStartup = true;
}

/**
* @param mixed ...$params
*/
public function beforeInvoke(...$params): void
{
$this->invokedBeforeInvoke = $params;
}

/**
* @param mixed ...$params
*/
public function afterInvoke(...$params): void
{
$this->invokedAfterInvoke = $params;
}
}