Files
XC_VM/tests/Unit/EventDispatcherTest.php
T
Divarion-D 4a0501324c feat(modules): evolve module system to extensible platform architecture
TASK-000: Remove legacy core patching system
- Delete src/core/Module/CoreCodePatcher.php
- Delete src/core/Module/CoreCodePatchableModuleInterface.php

TASK-001: Split ModuleInterface into specialized provider interfaces
- Add ServiceProviderInterface (boot, getEventSubscribers)
- Add RouteProviderInterface (registerRoutes)
- Add CommandProviderInterface (registerCommands)
- Add NavbarProviderInterface (registerNavbar)
- Add StreamMiddlewareProviderInterface
- ModuleLoader::bootAll() detects supported providers via instanceof

TASK-002: Refactor EventDispatcher
- Add ListenerProvider with priority support
- Add AbstractEvent base class
- Add StoppableEventInterface support
- Preserve legacy subscribe/publish API compatibility

TASK-003: Extend ServiceContainer
- Add ServiceContainer::decorate()
- Support priority-based decorator chains
- Add protected service registry:
  - db
  - settings
  - config
  - auth

TASK-004: Add core event catalog
- ModuleLoadedEvent
- ModuleBootedEvent
- PackageInstalledEvent
- UserAuthenticatedEvent
- UserLoggedOutEvent
- StreamStartingEvent
- StreamStartedEvent
- StreamStoppedEvent
- SettingsChangedEvent

TASK-005: Formalize subsystem boundaries
- Add BoundaryInterface
- Add MinistraBootstrap implementation
- Define exported service contracts

TASK-006: Add stream middleware pipeline
- Add StreamContext
- Add StreamMiddlewareInterface
- Add StreamPipeline
- Support middleware priority ordering

TASK-007: Add module hot reload
- Dispatch PackageInstalledEvent after installation
- Add ModuleManager::hotReload()
- Load and boot newly installed modules without service restart

TASK-008: Standardize container contracts
- Add container interfaces:
  - ContainerInterface
  - ContainerExceptionInterface
  - NotFoundExceptionInterface
- Add NotFoundException implementation
- ServiceContainer::get() throws NotFoundException

TASK-009: Refactor navbar registration
- CoreNavbarProvider implements NavbarProviderInterface
- Add registerNavbar() instance method
- ModuleLoader uses provider-based registration

TASK-010: Add module priority support
- Add priority field to module.json
- Default value: 0
- Higher priority modules load earlier
- Resolve load order by:
  1. Priority DESC
  2. Module name ASC

TASK-011: Add optional dependencies
- Add optional_dependencies field to module.json
- Missing optional dependencies do not block module loading
- Process optional dependencies after required dependencies

Tests
- Add ServiceContainer tests
- Add EventDispatcher tests
- Add StreamPipeline tests
- Add ModuleLoader priority tests

Documentation
- Rewrite module architecture documentation
- Update examples and extension development guides
- Document lifecycle, events, services, middleware, and dependencies
2026-06-14 14:25:19 +03:00

136 lines
4.7 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
final class EventDispatcherTest extends TestCase {
protected function setUp(): void {
EventDispatcher::clear();
}
protected function tearDown(): void {
EventDispatcher::clear();
}
// ── dispatch / listen ──────────────────────────────────────
public function testDispatchCallsMatchingListener(): void {
$received = null;
EventDispatcher::listen(TestPlainEvent::class, function (TestPlainEvent $e) use (&$received) {
$received = $e->value;
});
EventDispatcher::dispatch(new TestPlainEvent('hello'));
$this->assertSame('hello', $received);
}
public function testDispatchReturnsEvent(): void {
$event = new TestPlainEvent('x');
$returned = EventDispatcher::dispatch($event);
$this->assertSame($event, $returned);
}
public function testDispatchDoesNotCallUnrelatedListeners(): void {
$called = false;
EventDispatcher::listen(TestPlainEvent::class, function () use (&$called) {
$called = true;
});
EventDispatcher::dispatch(new TestOtherEvent());
$this->assertFalse($called);
}
// ── Priority ordering ──────────────────────────────────────
public function testListenersAreCalledInPriorityDescendingOrder(): void {
$order = [];
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'low'; }, 10);
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'high'; }, 50);
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'mid'; }, 30);
EventDispatcher::dispatch(new TestPlainEvent('x'));
$this->assertSame(['high', 'mid', 'low'], $order);
}
// ── StoppableEventInterface ────────────────────────────────
public function testStoppableEventStopsPropagation(): void {
$calls = 0;
EventDispatcher::listen(TestStoppableEvent::class, function (TestStoppableEvent $e) use (&$calls) {
$calls++;
$e->stopPropagation();
}, 10);
EventDispatcher::listen(TestStoppableEvent::class, function () use (&$calls) {
$calls++;
}, 5);
EventDispatcher::dispatch(new TestStoppableEvent());
$this->assertSame(1, $calls);
}
// ── hasListeners ───────────────────────────────────────────
public function testHasListenersReturnsTrueWhenRegistered(): void {
EventDispatcher::listen(TestPlainEvent::class, fn() => null);
$this->assertTrue(EventDispatcher::hasListeners(TestPlainEvent::class));
}
public function testHasListenersReturnsFalseWhenNone(): void {
$this->assertFalse(EventDispatcher::hasListeners(TestPlainEvent::class));
}
// ── unlisten / clear ───────────────────────────────────────
public function testUnlistenRemovesAllListenersForEvent(): void {
$called = false;
EventDispatcher::listen(TestPlainEvent::class, fn() => $called = true);
EventDispatcher::unlisten(TestPlainEvent::class);
EventDispatcher::dispatch(new TestPlainEvent('x'));
$this->assertFalse($called);
}
public function testClearRemovesAllListeners(): void {
$called = false;
EventDispatcher::listen(TestPlainEvent::class, fn() => $called = true);
EventDispatcher::clear();
EventDispatcher::dispatch(new TestPlainEvent('x'));
$this->assertFalse($called);
}
// ── Multiple listeners on same event ───────────────────────
public function testMultipleListenersAllCalled(): void {
$results = [];
EventDispatcher::listen(TestPlainEvent::class, function () use (&$results) { $results[] = 'a'; });
EventDispatcher::listen(TestPlainEvent::class, function () use (&$results) { $results[] = 'b'; });
EventDispatcher::dispatch(new TestPlainEvent('x'));
$this->assertCount(2, $results);
}
}
// ── Test event fixtures ────────────────────────────────────────────
final class TestPlainEvent {
public function __construct(public readonly string $value = '') {}
}
final class TestOtherEvent {}
final class TestStoppableEvent extends AbstractEvent {}