Skip to content

Commit

Permalink
Add event bus
Browse files Browse the repository at this point in the history
  • Loading branch information
brendt committed May 2, 2024
1 parent 82c7aa5 commit 13eeeef
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 3 deletions.
9 changes: 9 additions & 0 deletions app/Events/ItHappened.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Events;

final readonly class ItHappened
{
}
18 changes: 18 additions & 0 deletions app/Events/MyEventHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Events;

use Tempest\Events\EventHandler;

final class MyEventHandler
{
public static bool $itHappened = false;

#[EventHandler]
public function handleItHappened(ItHappened $event): void
{
self::$itHappened = true;
}
}
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"autoload": {
"psr-4": {
"Tempest\\": "src/"
}
},
"files": [
"src/functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
7 changes: 7 additions & 0 deletions src/Config/eventBus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

use Tempest\Events\EventBusConfig;

return new EventBusConfig();
78 changes: 78 additions & 0 deletions src/Discovery/EventBusDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Tempest\Discovery;

use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use Tempest\Container\Container;
use Tempest\Events\EventBusConfig;
use Tempest\Events\EventHandler;
use Tempest\Support\Reflection\Attributes;

final readonly class EventBusDiscovery implements Discovery
{
private const string CACHE_PATH = __DIR__ . '/event-bus-discovery.cache.php';

public function __construct(
private EventBusConfig $eventBusConfig,
) {
}

public function discover(ReflectionClass $class): void
{
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$eventHandler = Attributes::find(EventHandler::class)->in($method)->first();

if (! $eventHandler) {
continue;
}

$parameters = $method->getParameters();

if (count($parameters) !== 1) {
continue;
}

$type = $parameters[0]->getType();

if (! $type instanceof ReflectionNamedType) {
continue;
}

if (! class_exists($type->getName())) {
continue;
}

$this->eventBusConfig->addHandler(
eventHandler: $eventHandler,
eventName: $type->getName(),
reflectionMethod: $method,
);
}
}

public function hasCache(): bool
{
return file_exists(self::CACHE_PATH);
}

public function storeCache(): void
{
file_put_contents(self::CACHE_PATH, serialize($this->eventBusConfig->handlers));
}

public function restoreCache(Container $container): void
{
$handlers = unserialize(file_get_contents(self::CACHE_PATH));

$this->eventBusConfig->handlers = $handlers;
}

public function destroyCache(): void
{
@unlink(self::CACHE_PATH);
}
}
10 changes: 10 additions & 0 deletions src/Events/EventBus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

interface EventBus
{
public function dispatch(object $event): void;
}
35 changes: 35 additions & 0 deletions src/Events/EventBusConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

use ReflectionMethod;

final class EventBusConfig
{
public function __construct(
/** @var \Tempest\Events\EventHandler[][] */
public array $handlers = [],

/** @var \Tempest\Events\EventBusMiddleware[] */
public array $middleware = [],
) {
}

public function addHandler(EventHandler $eventHandler, string $eventName, ReflectionMethod $reflectionMethod): self
{
$this->handlers[$eventName][] = $eventHandler
->setEventName($eventName)
->setHandler($reflectionMethod);

return $this;
}

public function addMiddleware(EventBusMiddleware $middleware): self
{
$this->middleware[] = $middleware;

return $this;
}
}
21 changes: 21 additions & 0 deletions src/Events/EventBusInitializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Singleton;

#[Singleton]
final readonly class EventBusInitializer implements Initializer
{
public function initialize(Container $container): EventBus
{
return new GenericEventBus(
$container,
$container->get(EventBusConfig::class),
);
}
}
10 changes: 10 additions & 0 deletions src/Events/EventBusMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

interface EventBusMiddleware
{
public function __invoke(object $event, callable $next): void;
}
48 changes: 48 additions & 0 deletions src/Events/EventHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

use Attribute;
use ReflectionMethod;

#[Attribute]
final class EventHandler
{
public string $eventName;

public ReflectionMethod $handler;

public function setEventName(string $eventName): self
{
$this->eventName = $eventName;

return $this;
}

public function setHandler(ReflectionMethod $handler): self
{
$this->handler = $handler;

return $this;
}

public function __serialize(): array
{
return [
'eventName' => $this->eventName,
'handler_class' => $this->handler->getDeclaringClass()->getName(),
'handler_method' => $this->handler->getName(),
];
}

public function __unserialize(array $data): void
{
$this->eventName = $data['eventName'];
$this->handler = new ReflectionMethod(
objectOrMethod: $data['handler_class'],
method: $data['handler_method'],
);
}
}
47 changes: 47 additions & 0 deletions src/Events/GenericEventBus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Tempest\Events;

use Closure;
use Tempest\Container\Container;

final readonly class GenericEventBus implements EventBus
{
public function __construct(
private Container $container,
private EventBusConfig $eventBusConfig,
) {
}

public function dispatch(object $event): void
{
/** @var \Tempest\Events\EventHandler[] $eventHandlers */
$eventHandlers = $this->eventBusConfig->handlers[$event::class] ?? [];

foreach ($eventHandlers as $eventHandler) {
$callable = $this->getCallable($eventHandler);

$callable($event);
}
}

private function getCallable(EventHandler $eventHandler): Closure
{
$callable = function (object $event) use ($eventHandler) {
$eventHandler->handler->invoke(
$this->container->get($eventHandler->handler->getDeclaringClass()->getName()),
$event,
);
};

$middlewareStack = $this->eventBusConfig->middleware;

while ($middleware = array_pop($middlewareStack)) {
$callable = fn (object $event) => $this->container->get($middleware::class)($event, $callable);
}

return $callable;
}
}
28 changes: 28 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Tempest
{
use Tempest\Container\GenericContainer;
use Tempest\Events\EventBus;

/**
* @template TClassName
* @param class-string<TClassName> $className
* @return TClassName
*/
function get(string $className): object
{
$container = GenericContainer::instance();

return $container->get($className);
}

function event(object $event): void
{
$eventBus = get(EventBus::class);

$eventBus->dispatch($event);
}
}
51 changes: 51 additions & 0 deletions tests/Unit/Events/EventBusTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Unit\Events;

use App\Events\ItHappened;
use App\Events\MyEventHandler;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Tempest\Container\GenericContainer;
use Tempest\Events\EventBusConfig;
use Tempest\Events\EventHandler;
use Tempest\Events\GenericEventBus;
use Tests\Tempest\Unit\Events\Fixtures\MyEventBusMiddleware;

/**
* @internal
* @small
*/
class EventBusTest extends TestCase
{
public function test_it_works(): void
{
$container = new GenericContainer();

$handler = new EventHandler();
$handler->setHandler(new ReflectionMethod(MyEventHandler::class, 'handleItHappened'));

$config = new EventBusConfig(
handlers: [
ItHappened::class => [
$handler,
],
],
middleware: [
new MyEventBusMiddleware(),
]
);

$eventBus = new GenericEventBus($container, $config);

MyEventHandler::$itHappened = false;
MyEventBusMiddleware::$hit = false;

$eventBus->dispatch(new ItHappened());

$this->assertTrue(MyEventHandler::$itHappened);
$this->assertTrue(MyEventBusMiddleware::$hit);
}
}
Loading

0 comments on commit 13eeeef

Please sign in to comment.