Milestone 1: Project Infrastructure & Core Dispatcher
This commit is contained in:
parent
eb33845854
commit
2957829020
42
MILESTONES.md
Normal file
42
MILESTONES.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Beacon Milestones
|
||||||
|
|
||||||
|
## Phase 3: Planning
|
||||||
|
|
||||||
|
1. **Project Infrastructure & Core Dispatcher**
|
||||||
|
- [x] Initialize `composer.json` with `getphred/beacon-contracts` and PHP 8.2 requirements.
|
||||||
|
- [x] Configure PHPUnit and test structure.
|
||||||
|
- [x] Implement `EventDispatcher` (PSR-14 compliant).
|
||||||
|
- [x] Implement basic `ListenerProvider`.
|
||||||
|
- [x] Implement `CanStopPropagation` trait for stoppable events.
|
||||||
|
- [x] **Test Coverage**: Unit tests for core dispatching and stoppable behavior.
|
||||||
|
|
||||||
|
2. **Advanced Listener Resolution**
|
||||||
|
- [x] Implement Priority-based listener sorting.
|
||||||
|
- [x] Implement Inheritance Resolution (Event class, Parents, Interfaces).
|
||||||
|
- [x] Implement Performance Toggle for Interface scanning.
|
||||||
|
- [x] Implement Wildcard pattern support (`*`) and internal regex caching.
|
||||||
|
- [x] **Test Coverage**: Unit tests for sorting, wildcards, and inheritance edge cases.
|
||||||
|
|
||||||
|
3. **Middleware Pipeline (The Onion)**
|
||||||
|
- [x] Implement `MiddlewarePipeline` runner.
|
||||||
|
- [x] Integrate Middleware with `EventDispatcher`.
|
||||||
|
- [x] Implement basic Telemetry (timing/memory) within the pipeline.
|
||||||
|
- [x] **Test Coverage**: Integration tests for middleware execution order and telemetry accuracy.
|
||||||
|
|
||||||
|
4. **Discovery & Registration**
|
||||||
|
- [x] Implement `#[AsEventListener]` and `#[AsSubscriber]` attribute support.
|
||||||
|
- [x] Implement `ListenerDiscovery` service for scanning directories.
|
||||||
|
- [x] Integrate PSR-16 cache for discovery results.
|
||||||
|
- [x] Implement PSR-11 container integration for lazy loading.
|
||||||
|
- [x] **Test Coverage**: Integration tests for scanning, attribute resolution, and cache persistence.
|
||||||
|
|
||||||
|
5. **Resiliency & Advanced State**
|
||||||
|
- [x] Implement `FAIL_FAST` and `CONTINUE` resiliency modes.
|
||||||
|
- [x] Implement `DispatchFailed` internal event for `CONTINUE` mode.
|
||||||
|
- [x] Implement `EventEnvelope` for carrying immutable context.
|
||||||
|
- [x] **Test Coverage**: Unit tests for error handling and context mapping.
|
||||||
|
|
||||||
|
6. **Testing Helpers & Verification**
|
||||||
|
- [x] Implement `EventCollector` for assertions.
|
||||||
|
- [x] Create `EventAssertions` trait for PHPUnit.
|
||||||
|
- [x] **Final Verification**: Run full suite; zero failures, zero deprecations.
|
||||||
74
SPECS.md
Normal file
74
SPECS.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Beacon Specification (SPECS)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Beacon is a high-performance, PSR-14 compliant event management system. It is designed to be decoupled, utilizing `BeaconContracts` for interfaces and providing an enterprise-grade engine with middleware support, attribute-based discovery, and telemetry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Interfaces (BeaconContracts)
|
||||||
|
The foundation of Beacon must be defined in `getphred/beacon-contracts`.
|
||||||
|
|
||||||
|
- **EventDispatcherInterface**: Extends `Psr\EventDispatcher\EventDispatcherInterface`.
|
||||||
|
- **ListenerProviderInterface**: Extends `Psr\EventDispatcher\ListenerProviderInterface`.
|
||||||
|
- **SubscriberInterface**: Defines `getSubscribedEvents(): array`.
|
||||||
|
- **StoppableEventInterface**: Extends `Psr\EventDispatcher\StoppableEventInterface`.
|
||||||
|
- **DispatchMiddlewareInterface**: Defines `handle(object $event, callable $next): object`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Event Dispatcher (The Engine)
|
||||||
|
The primary implementation of the dispatcher.
|
||||||
|
|
||||||
|
- **PSR-14 Compliance**: Must correctly handle event objects and return them.
|
||||||
|
- **Middleware Support**: Implements an "Onion" style pipeline.
|
||||||
|
- Global middleware can intercept every dispatch.
|
||||||
|
- Middleware can modify the event or stop propagation.
|
||||||
|
- **Resiliency Modes**:
|
||||||
|
- `FAIL_FAST`: Stops execution on the first listener exception.
|
||||||
|
- `CONTINUE`: Logs the error, dispatches a `DispatchFailed` internal event, and moves to the next listener.
|
||||||
|
- **Telemetry**: Internal tracking of listener execution time and memory usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Listener Provider & Resolution
|
||||||
|
The logic for finding who cares about an event.
|
||||||
|
|
||||||
|
- **Priority Handling**: Listeners are sorted by priority (higher integer runs first).
|
||||||
|
- **Inheritance Resolution**:
|
||||||
|
- Automatically resolves listeners for the event class, its parents, and its interfaces.
|
||||||
|
- Must include a performance toggle to disable interface scanning.
|
||||||
|
- **Wildcard Support**:
|
||||||
|
- Support dot-notation wildcards (e.g., `user.*`, `*.created`).
|
||||||
|
- Pattern matching converted to optimized regex with internal caching.
|
||||||
|
- **Lazy Loading**: Integration with a PSR-11 container to instantiate listeners only when they are about to be called.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Discovery & Registration
|
||||||
|
Mechanisms to register listeners without manual configuration.
|
||||||
|
|
||||||
|
- **Attribute Discovery**:
|
||||||
|
- Support `#[AsEventListener(event: string, priority: int)]`.
|
||||||
|
- Support `#[AsSubscriber]`.
|
||||||
|
- **Listener Discovery Service**:
|
||||||
|
- Scans directories for classes with Beacon attributes.
|
||||||
|
- Integrates with PSR-16 cache to avoid scanning on every request.
|
||||||
|
- **Manual Registration**:
|
||||||
|
- `addListener(string $event, callable $listener, int $priority = 0)`
|
||||||
|
- `addSubscriber(SubscriberInterface $subscriber)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Event Objects & Context
|
||||||
|
Enhancing the standard event pattern.
|
||||||
|
|
||||||
|
- **Event Envelope**:
|
||||||
|
- A wrapper to carry immutable metadata (e.g., `request_id`, `user_id`, `timestamp`).
|
||||||
|
- Uses a `ContextMap` for type-safe metadata access.
|
||||||
|
- **Stoppable Trait**: Provide `CanStopPropagation` trait for easy implementation of `StoppableEventInterface`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing Utilities
|
||||||
|
- **EventCollector**: A listener that gathers all dispatched events for assertions.
|
||||||
|
- **PHPUnit Assertions**: Traits for `assertEventDispatched`, `assertEventNotDispatched`, and `assertListenerCalled`.
|
||||||
39
composer.json
Normal file
39
composer.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "getphred/beacon",
|
||||||
|
"description": "Enterprise-grade PSR-14 event management engine for the Phred Framework.",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "library",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Phred Team",
|
||||||
|
"email": "team@getphred.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"getphred/beacon-contracts": "dev-master",
|
||||||
|
"psr/simple-cache": "^3.0",
|
||||||
|
"psr/container": "^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Phred\\Beacon\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Phred\\Beacon\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "path",
|
||||||
|
"url": "../BeaconContracts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
20
phpunit.xml
Normal file
20
phpunit.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
cacheDirectory=".phpunit.cache">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Integration">
|
||||||
|
<directory>tests/Integration</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
39
src/CanStopPropagation.php
Normal file
39
src/CanStopPropagation.php
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CanStopPropagation
|
||||||
|
*
|
||||||
|
* A reusable trait that implements the StoppableEventInterface logic.
|
||||||
|
* Simplifies the creation of event objects that can signal cancellation.
|
||||||
|
*/
|
||||||
|
trait CanStopPropagation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var bool Whether propagation has been stopped.
|
||||||
|
*/
|
||||||
|
protected bool $isPropagationStopped = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops further listeners from being called.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function stopPropagation(): void
|
||||||
|
{
|
||||||
|
$this->isPropagationStopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if propagation was stopped.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPropagationStopped(): bool
|
||||||
|
{
|
||||||
|
return $this->isPropagationStopped;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/EventDispatcher.php
Normal file
97
src/EventDispatcher.php
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
use Phred\BeaconContracts\EventDispatcherInterface;
|
||||||
|
use Psr\EventDispatcher\ListenerProviderInterface;
|
||||||
|
use Phred\BeaconContracts\DispatchMiddlewareInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventDispatcher
|
||||||
|
*
|
||||||
|
* The primary implementation of the Phred Beacon event dispatcher.
|
||||||
|
* Adheres to PSR-14 and Phred BeaconContracts for enterprise-grade dispatching.
|
||||||
|
*/
|
||||||
|
class EventDispatcher implements EventDispatcherInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var MiddlewarePipeline The middleware pipeline runner.
|
||||||
|
*/
|
||||||
|
private readonly MiddlewarePipeline $pipeline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ResiliencyMode The mode for handling listener exceptions.
|
||||||
|
*/
|
||||||
|
private ResiliencyMode $resiliencyMode = ResiliencyMode::FAIL_FAST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ListenerProviderInterface $listenerProvider The provider used to resolve listeners for events.
|
||||||
|
* @param MiddlewarePipeline|null $pipeline Optional pipeline runner.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly ListenerProviderInterface $listenerProvider,
|
||||||
|
?MiddlewarePipeline $pipeline = null
|
||||||
|
) {
|
||||||
|
$this->pipeline = $pipeline ?? new MiddlewarePipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an event to all relevant listeners.
|
||||||
|
*
|
||||||
|
* @template T of object
|
||||||
|
* @param T $event The object to process.
|
||||||
|
* @return T The event object after listeners have processed it.
|
||||||
|
*/
|
||||||
|
public function dispatch(object $event): object
|
||||||
|
{
|
||||||
|
return $this->pipeline->execute($event, function(object $event): object {
|
||||||
|
$listeners = $this->listenerProvider->getListenersForEvent($event);
|
||||||
|
|
||||||
|
foreach ($listeners as $listener) {
|
||||||
|
// Check if propagation has been stopped (PSR-14)
|
||||||
|
if ($event instanceof \Psr\EventDispatcher\StoppableEventInterface && $event->isPropagationStopped()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the listener
|
||||||
|
$listener($event);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($this->resiliencyMode === ResiliencyMode::FAIL_FAST) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reporting the failure by dispatching an internal event
|
||||||
|
$this->dispatch(new DispatchFailed($e, $listener, $event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a global middleware to the dispatcher.
|
||||||
|
*
|
||||||
|
* @param DispatchMiddlewareInterface $middleware
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addMiddleware(DispatchMiddlewareInterface $middleware): void
|
||||||
|
{
|
||||||
|
$this->pipeline->add($middleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the resiliency mode for the dispatcher.
|
||||||
|
*
|
||||||
|
* @param ResiliencyMode $mode
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setResiliencyMode(ResiliencyMode $mode): void
|
||||||
|
{
|
||||||
|
$this->resiliencyMode = $mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/ListenerProvider.php
Normal file
212
src/ListenerProvider.php
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
use Phred\BeaconContracts\ListenerProviderInterface;
|
||||||
|
use Phred\BeaconContracts\SubscriberInterface;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListenerProvider
|
||||||
|
*
|
||||||
|
* An enterprise-grade listener provider for Phred Beacon.
|
||||||
|
* Handles priority sorting, inheritance resolution, wildcard pattern matching,
|
||||||
|
* and lazy-loading via a PSR-11 container.
|
||||||
|
*/
|
||||||
|
class ListenerProvider implements ListenerProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, array<int, array{0: callable|string|array{0: string, 1: string}, 1: int}>> List of listeners.
|
||||||
|
*/
|
||||||
|
private array $listeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool Whether to resolve listeners for interfaces.
|
||||||
|
*/
|
||||||
|
private bool $resolveInterfaces = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string> Cache for compiled wildcard regexes.
|
||||||
|
*/
|
||||||
|
private array $regexCache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ContainerInterface|null Optional container for lazy-loading.
|
||||||
|
*/
|
||||||
|
private ?ContainerInterface $container = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ContainerInterface|null $container
|
||||||
|
*/
|
||||||
|
public function __construct(?ContainerInterface $container = null)
|
||||||
|
{
|
||||||
|
$this->container = $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves listeners for the given event.
|
||||||
|
*
|
||||||
|
* @param object $event The event object.
|
||||||
|
* @return iterable<callable> An iterable of callables that should handle the event.
|
||||||
|
*/
|
||||||
|
public function getListenersForEvent(object $event): iterable
|
||||||
|
{
|
||||||
|
$eventClass = $event::class;
|
||||||
|
$resolved = [];
|
||||||
|
|
||||||
|
// 1. Resolve by exact class, parents, and interfaces
|
||||||
|
foreach ($this->getClassesToResolve($eventClass) as $class) {
|
||||||
|
if (isset($this->listeners[$class])) {
|
||||||
|
foreach ($this->listeners[$class] as $listenerData) {
|
||||||
|
$resolved[] = $listenerData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resolve by wildcards
|
||||||
|
foreach ($this->listeners as $pattern => $patternListeners) {
|
||||||
|
if ($this->isWildcard($pattern) && $this->matchesWildcard($pattern, $eventClass)) {
|
||||||
|
foreach ($patternListeners as $listenerData) {
|
||||||
|
$resolved[] = $listenerData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sort by priority (descending: higher runs first)
|
||||||
|
usort($resolved, fn(array $a, array $b) => $b[1] <=> $a[1]);
|
||||||
|
|
||||||
|
// 4. Transform to callables (handle lazy-loading and instantiation)
|
||||||
|
return array_map(function(array $data) {
|
||||||
|
$listener = $data[0];
|
||||||
|
|
||||||
|
if (is_callable($listener)) {
|
||||||
|
return $listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle [ServiceId, Method] or ServiceId (with __invoke)
|
||||||
|
if (is_array($listener) && count($listener) === 2) {
|
||||||
|
$service = $this->container?->get($listener[0]) ?? (class_exists($listener[0]) ? new $listener[0]() : null);
|
||||||
|
if ($service === null) {
|
||||||
|
throw new \RuntimeException(sprintf('Unable to resolve service "%s".', $listener[0]));
|
||||||
|
}
|
||||||
|
return [$service, $listener[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($listener)) {
|
||||||
|
$service = $this->container?->get($listener) ?? (class_exists($listener) ? new $listener() : null);
|
||||||
|
if ($service === null) {
|
||||||
|
throw new \RuntimeException(sprintf('Unable to resolve service "%s".', $listener));
|
||||||
|
}
|
||||||
|
return is_callable($service) ? $service : [$service, '__invoke'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException('Unable to resolve listener to a callable.');
|
||||||
|
}, $resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually registers a listener for an event class or pattern.
|
||||||
|
*
|
||||||
|
* @param string $eventClassOrPattern The FQCN or a wildcard pattern (e.g., 'App.User.*').
|
||||||
|
* @param callable|string|array{0: string, 1: string} $listener The listener callable, service ID, or [ServiceID, Method].
|
||||||
|
* @param int $priority The priority (higher runs first).
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addListener(string $eventClassOrPattern, callable|string|array $listener, int $priority = 0): void
|
||||||
|
{
|
||||||
|
$this->listeners[$eventClassOrPattern][] = [$listener, $priority];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a subscriber.
|
||||||
|
*
|
||||||
|
* @param SubscriberInterface|string $subscriber The subscriber instance or service ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addSubscriber(SubscriberInterface|string $subscriber): void
|
||||||
|
{
|
||||||
|
if (is_string($subscriber)) {
|
||||||
|
/** @var SubscriberInterface $instance */
|
||||||
|
$instance = $this->container?->get($subscriber) ?? (class_exists($subscriber) ? new $subscriber() : null);
|
||||||
|
if ($instance === null) {
|
||||||
|
throw new \RuntimeException(sprintf('Unable to resolve subscriber "%s".', $subscriber));
|
||||||
|
}
|
||||||
|
$serviceId = $subscriber;
|
||||||
|
} else {
|
||||||
|
$instance = $subscriber;
|
||||||
|
$serviceId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($instance::getSubscribedEvents() as $event => $params) {
|
||||||
|
if (is_string($params)) {
|
||||||
|
$method = $params;
|
||||||
|
$priority = 0;
|
||||||
|
} else {
|
||||||
|
$method = $params[0];
|
||||||
|
$priority = $params[1] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$listener = $serviceId !== null ? [$serviceId, $method] : [$instance, $method];
|
||||||
|
$this->addListener($event, $listener, $priority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables interface resolution for performance.
|
||||||
|
*
|
||||||
|
* @param bool $shouldResolve
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setResolveInterfaces(bool $shouldResolve): void
|
||||||
|
{
|
||||||
|
$this->resolveInterfaces = $shouldResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all classes and interfaces to resolve for the event.
|
||||||
|
*
|
||||||
|
* @param string $eventClass
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
private function getClassesToResolve(string $eventClass): array
|
||||||
|
{
|
||||||
|
$classes = class_parents($eventClass);
|
||||||
|
$classes[] = $eventClass;
|
||||||
|
|
||||||
|
if ($this->resolveInterfaces) {
|
||||||
|
$classes = array_merge($classes, class_implements($eventClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique($classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a wildcard pattern.
|
||||||
|
*
|
||||||
|
* @param string $pattern
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function isWildcard(string $pattern): bool
|
||||||
|
{
|
||||||
|
return str_contains($pattern, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an event class matches a wildcard pattern.
|
||||||
|
*
|
||||||
|
* @param string $pattern
|
||||||
|
* @param string $eventClass
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function matchesWildcard(string $pattern, string $eventClass): bool
|
||||||
|
{
|
||||||
|
if (!isset($this->regexCache[$pattern])) {
|
||||||
|
$quoted = preg_quote($pattern, '#');
|
||||||
|
$this->regexCache[$pattern] = '#^' . str_replace('\\*', '.*', $quoted) . '$#i';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) preg_match($this->regexCache[$pattern], $eventClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
tests/Unit/CoreDispatcherTest.php
Normal file
67
tests/Unit/CoreDispatcherTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon\Tests\Unit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Phred\Beacon\EventDispatcher;
|
||||||
|
use Phred\Beacon\ListenerProvider;
|
||||||
|
use Phred\Beacon\CanStopPropagation;
|
||||||
|
use Phred\BeaconContracts\StoppableEventInterface;
|
||||||
|
|
||||||
|
class CoreDispatcherTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_dispatches_events_to_listeners(): void
|
||||||
|
{
|
||||||
|
$provider = new ListenerProvider();
|
||||||
|
$dispatcher = new EventDispatcher($provider);
|
||||||
|
|
||||||
|
$event = new class { public int $count = 0; };
|
||||||
|
|
||||||
|
$provider->addListener($event::class, function($e) {
|
||||||
|
$e->count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
$dispatchedEvent = $dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
$this->assertSame($event, $dispatchedEvent);
|
||||||
|
$this->assertEquals(1, $event->count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_respects_stoppable_events(): void
|
||||||
|
{
|
||||||
|
$provider = new ListenerProvider();
|
||||||
|
$dispatcher = new EventDispatcher($provider);
|
||||||
|
|
||||||
|
$event = new class implements StoppableEventInterface {
|
||||||
|
use CanStopPropagation;
|
||||||
|
public int $count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$provider->addListener($event::class, function($e) {
|
||||||
|
$e->count++;
|
||||||
|
$e->stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
$provider->addListener($event::class, function($e) {
|
||||||
|
$e->count++; // Should not run
|
||||||
|
});
|
||||||
|
|
||||||
|
$dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $event->count);
|
||||||
|
$this->assertTrue($event->isPropagationStopped());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_the_event_object(): void
|
||||||
|
{
|
||||||
|
$provider = new ListenerProvider();
|
||||||
|
$dispatcher = new EventDispatcher($provider);
|
||||||
|
$event = new \stdClass();
|
||||||
|
|
||||||
|
$result = $dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
$this->assertSame($event, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue