Skip to content

Commit

Permalink
Merge pull request #1700 from brefphp/more-hooks
Browse files Browse the repository at this point in the history
Rework the low-level hook system used by framework integrations
  • Loading branch information
mnapoli authored Nov 24, 2023
2 parents 2a6cd25 + 81e3ddc commit 4f0b8c3
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 0 deletions.
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;
}
}

0 comments on commit 4f0b8c3

Please sign in to comment.