diff --git a/.gitignore b/.gitignore index 7ffc7fa..325dd6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ .vscode/ *.swp *.swo -.DS_Store \ No newline at end of file +.DS_Store +composer.lock +/public/ \ No newline at end of file diff --git a/_config/events.yml b/_config/events.yml index 8a64c6e..6579fbe 100644 --- a/_config/events.yml +++ b/_config/events.yml @@ -4,7 +4,20 @@ After: - '#coreservices' --- SilverStripe\Core\Injector\Injector: - ArchiPro\Silverstripe\EventDispatcher\Service\EventService: + # Define the listener provider + ArchiPro\EventDispatcher\ListenerProvider: + class: ArchiPro\EventDispatcher\ListenerProvider + + # Default event dispatcher + ArchiPro\EventDispatcher\AsyncEventDispatcher: + class: ArchiPro\EventDispatcher\AsyncEventDispatcher constructor: - dispatcher: '%$Psr\EventDispatcher\EventDispatcherInterface' - listenerProvider: '%$Psr\EventDispatcher\ListenerProviderInterface' \ No newline at end of file + listenerProvider: '%$ArchiPro\EventDispatcher\ListenerProvider' + Psr\EventDispatcher\EventDispatcherInterface: + alias: '%$ArchiPro\EventDispatcher\AsyncEventDispatcher' + + # Bootstrap the event service + ArchiPro\Silverstripe\EventDispatcher\Service\EventService: + constructor: + dispatcher: '%$ArchiPro\EventDispatcher\AsyncEventDispatcher' + listenerProvider: '%$ArchiPro\EventDispatcher\ListenerProvider' \ No newline at end of file diff --git a/composer.json b/composer.json index 446aad3..d92c087 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,21 @@ { - "name": "archipro/silverstripe-event-dispatcher", - "description": "PSR-14 Event Dispatcher integration for Silverstripe CMS", + "name": "archipro/silverstripe-revolt-event-dispatcher", + "description": "A Revolt Event Dispatcher integration for Silverstripe CMS", "type": "silverstripe-vendormodule", "license": "MIT", "require": { "php": "^8.1", "silverstripe/framework": "^4.13 || ^5.0", "silverstripe/versioned": "^1.13 || ^2.0", - "psr/event-dispatcher": "^1.0" + "psr/event-dispatcher": "^1.0", + "psr/event-dispatcher-implementation": "^1.0", + "archipro/revolt-event-dispatcher": "dev-master" }, "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.0", "friendsofphp/php-cs-fixer": "^3.0", - "phpstan/phpstan": "^1.10", - "symbiote/silverstripe-phpstan": "^1.0" + "phpstan/phpstan": "^1.10" }, "autoload": { "psr-4": { @@ -36,5 +37,17 @@ "prefer-stable": true, "extra": { "expose": [] - } -} \ No newline at end of file + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "silverstripe/vendor-plugin": true + } + }, + "repositories": [ + { + "type": "github", + "url": "git@github.com:archiprocode/revolt-event-dispatcher.git" + } + ] +} diff --git a/src/Contract/ListenerLoaderInterface.php b/src/Contract/ListenerLoaderInterface.php new file mode 100644 index 0000000..bde31fb --- /dev/null +++ b/src/Contract/ListenerLoaderInterface.php @@ -0,0 +1,25 @@ +[] $classes Array of DataObject class names to listen for + * @param Operation[]|null $operations Array of operations to listen for. If null, listens for all operations. + */ + public function __construct( + private Closure $callback, + private array $classes, + private ?array $operations = null + ) { + $this->operations = $operations ?? Operation::cases(); + } + + /** + * Handles a DataObject event. + * + * Checks if the event matches the configured operations and classes, + * and executes the callback if it does. + * + * @param DataObjectEvent $event The event to handle + */ + public function __invoke(DataObjectEvent $event): void + { + // Check if we should handle this class + if (!$this->shouldHandleClass($event->getObjectClass())) { + return; + } + + // Check if we should handle this operation + if (!in_array($event->getOperation(), $this->operations)) { + return; + } + + // Execute callback + call_user_func($this->callback, $event); + } + + /** + * Checks if the given class matches any of the configured target classes. + * + * A match occurs if the class is either the same as or a subclass of any target class. + * + * @param string $class The class name to check + * @return bool True if the class should be handled, false otherwise + */ + private function shouldHandleClass(string $class): bool + { + foreach ($this->classes as $targetClass) { + if (is_a($class, $targetClass, true)) { + return true; + } + } + return false; + } +} diff --git a/src/Event/AbstractDataObjectEvent.php b/src/Event/AbstractDataObjectEvent.php deleted file mode 100644 index 0e989e6..0000000 --- a/src/Event/AbstractDataObjectEvent.php +++ /dev/null @@ -1,52 +0,0 @@ -objectID; - } - - public function getObjectClass(): string - { - return $this->objectClass; - } - - public function getAction(): string - { - return $this->action; - } - - public function getChanges(): array - { - return $this->changes; - } - - public function jsonSerialize(): array - { - return [ - 'id' => $this->objectID, - 'class' => $this->objectClass, - 'action' => $this->action, - 'changes' => $this->changes, - 'timestamp' => time(), - ]; - } -} \ No newline at end of file diff --git a/src/Event/DataObjectDeleteEvent.php b/src/Event/DataObjectDeleteEvent.php deleted file mode 100644 index 258267f..0000000 --- a/src/Event/DataObjectDeleteEvent.php +++ /dev/null @@ -1,13 +0,0 @@ -ID, + * get_class($dataObject), + * Operation::UPDATE, + * $dataObject->Version, + * Security::getCurrentUser()?->ID + * ); + * ``` + */ class DataObjectEvent { - private DataObject $dataObject; - private string $action; + use Injectable; - public function __construct(DataObject $dataObject, string $action) + /** + * @var int Unix timestamp when the event was created + */ + private readonly int $timestamp; + + /** + * @param int $objectID The ID of the affected DataObject + * @param string $objectClass The class name of the affected DataObject + * @param Operation $operation The type of operation performed + * @param int|null $version The version number (for versioned objects) + * @param int|null $memberID The ID of the member who performed the operation + */ + public function __construct( + private readonly int $objectID, + private readonly string $objectClass, + private readonly Operation $operation, + private readonly ?int $version = null, + private readonly ?int $memberID = null + ) { + $this->timestamp = time(); + } + + /** + * Get the ID of the affected DataObject + */ + public function getObjectID(): int + { + return $this->objectID; + } + + /** + * Get the class name of the affected DataObject + */ + public function getObjectClass(): string + { + return $this->objectClass; + } + + /** + * Get the type of operation performed + */ + public function getOperation(): Operation { - $this->dataObject = $dataObject; - $this->action = $action; + return $this->operation; + } + + /** + * Get the version number (for versioned objects) + */ + public function getVersion(): ?int + { + return $this->version; + } + + /** + * Get the ID of the member who performed the operation + */ + public function getMemberID(): ?int + { + return $this->memberID; + } + + /** + * Get the timestamp when the event was created + */ + public function getTimestamp(): int + { + return $this->timestamp; + } + + /** + * Get the DataObject associated with this event + * + * @param bool $useVersion If true and the object is versioned, retrieves the specific version that was affected + * Note: This may return null if the object has been deleted since the event was created + */ + public function getObject(bool $useVersion = false): ?DataObject + { + if (!$this->objectID) { + return null; + } + + $object = DataObject::get_by_id($this->objectClass, $this->objectID); + + // If we want the specific version and the object is versioned + if ($useVersion && $this->version && $object && $object->hasExtension(Versioned::class)) { + /** @var Versioned|DataObject $object */ + return $object->Version == $this->version + ? $object + : $object->Versions()->byID($this->version); + } + + return $object; + } + + /** + * Get the Member who performed the operation + * + * Note: This may return null if the member has been deleted since the event was created + * or if the operation was performed by a system process + */ + public function getMember(): ?Member + { + if (!$this->memberID) { + return null; + } + + return Member::get()->byID($this->memberID); } - public function getDataObject(): DataObject + /** + * Serialize the event to a string + */ + public function serialize(): string { - return $this->dataObject; + return serialize([ + 'objectID' => $this->objectID, + 'objectClass' => $this->objectClass, + 'operation' => $this->operation, + 'version' => $this->version, + 'memberID' => $this->memberID, + 'timestamp' => $this->timestamp, + ]); } - public function getAction(): string + /** + * Unserialize the event from a string + * + * @param string $data + */ + public function unserialize(string $data): void { - return $this->action; + $unserialized = unserialize($data); + + // Use reflection to set readonly properties + $reflection = new \ReflectionClass($this); + + foreach ($unserialized as $property => $value) { + $prop = $reflection->getProperty($property); + $prop->setAccessible(true); + $prop->setValue($this, $value); + } } } \ No newline at end of file diff --git a/src/Event/DataObjectVersionEvent.php b/src/Event/DataObjectVersionEvent.php deleted file mode 100644 index 211a2e1..0000000 --- a/src/Event/DataObjectVersionEvent.php +++ /dev/null @@ -1,37 +0,0 @@ -version; - } - - public function jsonSerialize(): array - { - return array_merge(parent::jsonSerialize(), [ - 'version' => $this->version, - ]); - } -} \ No newline at end of file diff --git a/src/Event/DataObjectWriteEvent.php b/src/Event/DataObjectWriteEvent.php deleted file mode 100644 index 1609a5f..0000000 --- a/src/Event/DataObjectWriteEvent.php +++ /dev/null @@ -1,16 +0,0 @@ -originalData = $this->owner->exists() ? $this->owner->getQueriedDatabaseFields() : []; - } - /** * Fires an event after the object is written (created or updated) */ public function onAfterWrite(): void { - // Don't fire write events during deletion process - if ($this->isSoftDelete) { - return; - } - - $event = new DataObjectWriteEvent( - $this->owner->ID, - get_class($this->owner), - $this->owner->isInDB() ? 'update' : 'create', - $this->getChanges() + $owner = $this->getOwner(); + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + // By this point isInDB() will return true even for new records since the ID is already set + // Instead check if the ID field was changed which indicates this is a new record + $owner->isChanged('ID') ? Operation::CREATE : Operation::UPDATE, + $owner->hasExtension(Versioned::class) ? $owner->Version : null, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); @@ -62,51 +40,37 @@ public function onAfterWrite(): void /** * Fires before a DataObject is deleted from the database - * For versioned objects, this is called during both soft and hard deletes */ public function onBeforeDelete(): void { - $isVersioned = $this->owner->hasExtension(Versioned::class); - $this->isSoftDelete = $isVersioned && !$this->owner->getIsDeleteFromStage(); - - $event = new DataObjectDeleteEvent( - $this->owner->ID, - get_class($this->owner), - $this->isSoftDelete ? 'soft_delete' : 'hard_delete', - [ - 'is_versioned' => $isVersioned, - 'deleted_from_stage' => $this->owner->getIsDeleteFromStage(), - 'version' => $isVersioned ? $this->owner->Version : null, - ] + $owner = $this->getOwner(); + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + Operation::DELETE, + $owner->hasExtension(Versioned::class) ? $owner->Version : null, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); } - /** - * Fires after a DataObject is deleted from the database - */ - public function onAfterDelete(): void - { - // Reset the soft delete flag - $this->isSoftDelete = false; - } - /** * Fires when a versioned DataObject is published */ public function onAfterPublish(): void { - if (!$this->owner->hasExtension(Versioned::class)) { + $owner = $this->getOwner(); + if (!$owner->hasExtension(Versioned::class)) { return; } - $event = new DataObjectVersionEvent( - $this->owner->ID, - get_class($this->owner), - 'publish', - $this->getChanges(), - $this->owner->Version + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + Operation::PUBLISH, + $owner->Version, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); @@ -117,16 +81,17 @@ public function onAfterPublish(): void */ public function onAfterUnpublish(): void { - if (!$this->owner->hasExtension(Versioned::class)) { + $owner = $this->getOwner(); + if (!$owner->hasExtension(Versioned::class)) { return; } - $event = new DataObjectVersionEvent( - $this->owner->ID, - get_class($this->owner), - 'unpublish', - [], - $this->owner->Version + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + Operation::UNPUBLISH, + $owner->Version, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); @@ -137,16 +102,17 @@ public function onAfterUnpublish(): void */ public function onAfterArchive(): void { - if (!$this->owner->hasExtension(Versioned::class)) { + $owner = $this->getOwner(); + if (!$owner->hasExtension(Versioned::class)) { return; } - $event = new DataObjectVersionEvent( - $this->owner->ID, - get_class($this->owner), - 'archive', - [], - $this->owner->Version + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + Operation::ARCHIVE, + $owner->Version, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); @@ -157,54 +123,26 @@ public function onAfterArchive(): void */ public function onAfterRestore(): void { - if (!$this->owner->hasExtension(Versioned::class)) { + $owner = $this->getOwner(); + if (!$owner->hasExtension(Versioned::class)) { return; } - $event = new DataObjectVersionEvent( - $this->owner->ID, - get_class($this->owner), - 'restore', - [], - $this->owner->Version + $event = DataObjectEvent::create( + $owner->ID, + get_class($owner), + Operation::RESTORE, + $owner->Version, + Security::getCurrentUser()?->ID ); $this->dispatchEvent($event); } - /** - * Calculates the changes made to the object by comparing original and new state - * - * @return array Array of changes with 'old' and 'new' values for each changed field - */ - protected function getChanges(): array - { - if (empty($this->originalData)) { - return $this->owner->toMap(); - } - - $changes = []; - $newData = $this->owner->toMap(); - - foreach ($newData as $field => $value) { - if (!isset($this->originalData[$field]) || $this->originalData[$field] !== $value) { - $changes[$field] = [ - 'old' => $this->originalData[$field] ?? null, - 'new' => $value - ]; - } - } - - return $changes; - } - /** * Dispatches an event using the EventService - * - * @param object $event The event to dispatch - * @return object The processed event */ - protected function dispatchEvent(object $event): object + protected function dispatchEvent(DataObjectEvent $event): DataObjectEvent { return Injector::inst()->get(EventService::class)->dispatch($event); } diff --git a/src/Service/EventService.php b/src/Service/EventService.php index 9dae391..9fb133b 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -2,46 +2,95 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Service; -use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\EventDispatcher\ListenerProviderInterface; +use ArchiPro\EventDispatcher\AsyncEventDispatcher; +use ArchiPro\EventDispatcher\ListenerProvider; +use ArchiPro\Silverstripe\EventDispatcher\Contract\ListenerLoaderInterface; use SilverStripe\Core\Injector\Injectable; +use SilverStripe\Core\Config\Configurable; /** * Core service class for handling event dispatching in Silverstripe. * * This service wraps a PSR-14 compliant event dispatcher and provides * a centralized way to dispatch events throughout the application. - * - * @property EventDispatcherInterface $dispatcher - * @property ListenerProviderInterface $listenerProvider */ class EventService { use Injectable; + use Configurable; - /** @var EventDispatcherInterface */ - private $dispatcher; - - /** @var ListenerProviderInterface */ - private $listenerProvider; + /** + * @config + * @var array> Map of event class names to arrays of listener callbacks + */ + private static array $listeners = []; /** - * @param EventDispatcherInterface $dispatcher PSR-14 event dispatcher implementation - * @param ListenerProviderInterface $listenerProvider PSR-14 listener provider implementation + * @config + * @var array Array of listener loaders */ + private static array $loaders = []; + public function __construct( - EventDispatcherInterface $dispatcher, - ListenerProviderInterface $listenerProvider + private readonly AsyncEventDispatcher $dispatcher, + private readonly ListenerProvider $listenerProvider ) { - $this->dispatcher = $dispatcher; - $this->listenerProvider = $listenerProvider; + $this->registerListeners(); + $this->loadListeners(); + } + + /** + * Registers listeners from the configuration + */ + private function registerListeners(): void + { + $listeners = $this->config()->get('listeners'); + if (empty($listeners)) { + return; + } + + foreach ($listeners as $eventClass => $listeners) { + foreach ($listeners as $listener) { + $this->addListener($eventClass, $listener); + } + } + } + + /** + * Loads listeners from the configuration + */ + private function loadListeners(): void + { + foreach ($this->config()->get('loaders') as $loader) { + $this->addListenerLoader($loader); + } + } + + /** + * Adds a listener to the event service + */ + public function addListener(string $event, callable $listener): void + { + $this->listenerProvider->addListener($event, $listener); + } + + /** + * Adds a listener loader to the event service + * @throws \RuntimeException If the loader does not implement ListenerLoaderInterface + */ + public function addListenerLoader(ListenerLoaderInterface $loader): void + { + if (!$loader instanceof ListenerLoaderInterface) { + throw new \RuntimeException(sprintf( + 'Listener loader class "%s" must implement ListenerLoaderInterface', + get_class($loader) + )); + } + $loader->loadListeners($this->listenerProvider); } /** * Dispatches an event to all registered listeners - * - * @param object $event The event to dispatch - * @return object The event after it has been processed by all listeners */ public function dispatch(object $event): object { @@ -50,11 +99,9 @@ public function dispatch(object $event): object /** * Gets the listener provider instance - * - * @return ListenerProviderInterface */ - public function getListenerProvider(): ListenerProviderInterface + public function getListenerProvider(): ListenerProvider { return $this->listenerProvider; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/tests/Event/DataObjectEventTest.php b/tests/Event/DataObjectEventTest.php new file mode 100644 index 0000000..ae6ffb7 --- /dev/null +++ b/tests/Event/DataObjectEventTest.php @@ -0,0 +1,96 @@ +assertEquals(1, $event->getObjectID()); + $this->assertEquals(SimpleDataObject::class, $event->getObjectClass()); + $this->assertEquals(Operation::CREATE, $event->getOperation()); + $this->assertNull($event->getVersion()); + $this->assertEquals(1, $event->getMemberID()); + $this->assertGreaterThan(0, $event->getTimestamp()); + } + + public function testGetObject(): void + { + /** @var SimpleDataObject $object */ + $object = $this->objFromFixture(SimpleDataObject::class, 'object1'); + + $event = DataObjectEvent::create($object->ID, SimpleDataObject::class, Operation::UPDATE); + + $this->assertNotNull($event->getObject()); + $this->assertEquals($object->ID, $event->getObject()->ID); + } + + public function testGetVersionedObject(): void + { + /** @var VersionedDataObject $object */ + $object = $this->objFromFixture(VersionedDataObject::class, 'versioned1'); + + // Create a new version + $object->Title = 'Updated Title'; + $object->write(); + + $event = DataObjectEvent::create($object->ID, VersionedDataObject::class, Operation::UPDATE, $object->Version); + + // Get current version + $currentObject = $event->getObject(false); + $this->assertEquals('Updated Title', $currentObject->Title); + + // Get specific version + $versionedObject = $event->getObject(true); + $this->assertEquals('Updated Title', $versionedObject->Title); + + // Get previous version + $previousEvent = DataObjectEvent::create($object->ID, VersionedDataObject::class, Operation::UPDATE, $object->Version - 1); + $previousVersion = $previousEvent->getObject(true); + $this->assertEquals('Original Title', $previousVersion->Title); + } + + public function testGetMember(): void + { + /** @var Member $member */ + $member = $this->objFromFixture(Member::class, 'member1'); + + $event = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE, null, $member->ID); + + $this->assertNotNull($event->getMember()); + $this->assertEquals($member->ID, $event->getMember()->ID); + } + + public function testSerialization(): void + { + $event = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE, 2, 3); + + $serialized = serialize($event); + /** @var DataObjectEvent $unserialized */ + $unserialized = unserialize($serialized); + + $this->assertEquals(1, $unserialized->getObjectID()); + $this->assertEquals(SimpleDataObject::class, $unserialized->getObjectClass()); + $this->assertEquals(Operation::CREATE, $unserialized->getOperation()); + $this->assertEquals(2, $unserialized->getVersion()); + $this->assertEquals(3, $unserialized->getMemberID()); + $this->assertEquals($event->getTimestamp(), $unserialized->getTimestamp()); + } +} \ No newline at end of file diff --git a/tests/Event/DataObjectEventTest.yml b/tests/Event/DataObjectEventTest.yml new file mode 100644 index 0000000..deb1782 --- /dev/null +++ b/tests/Event/DataObjectEventTest.yml @@ -0,0 +1,13 @@ +ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\SimpleDataObject: + object1: + Title: 'Test Object' + +ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\VersionedDataObject: + versioned1: + Title: 'Original Title' + +SilverStripe\Security\Member: + member1: + FirstName: 'Test' + Surname: 'User' + Email: 'test@example.com' \ No newline at end of file diff --git a/tests/Extension/EventDispatchExtensionTest.php b/tests/Extension/EventDispatchExtensionTest.php new file mode 100644 index 0000000..a0088c8 --- /dev/null +++ b/tests/Extension/EventDispatchExtensionTest.php @@ -0,0 +1,125 @@ +get(EventService::class); + + // Add listener that captures events + $service->addListener(DataObjectEvent::class, new DataObjectEventListener( + function (DataObjectEvent $event) { + static::$events[] = $event; + }, + [SimpleDataObject::class, VersionedDataObject::class] + )); + } + + protected function setUp(): void + { + parent::setUp(); + static::$events = []; + } + + + public function testWriteEvents(): void + { + // Test create + $object = SimpleDataObject::create(['Title' => 'Test']); + $object->write(); + EventLoop::run(); + + $this->assertCount(1, static::$events); + $this->assertEquals(Operation::CREATE, static::$events[0]->getOperation()); + + // Clear events + static::$events = []; + + // Test update + $object->Title = 'Updated'; + $object->write(); + + EventLoop::run(); + + $this->assertCount(1, static::$events); + $this->assertEquals(Operation::UPDATE, static::$events[0]->getOperation()); + } + + public function testDeleteEvent(): void + { + $object = SimpleDataObject::create(['Title' => 'Test']); + $object->write(); + EventLoop::run(); + + static::$events = []; + $object->delete(); + EventLoop::run(); + + $this->assertCount(1, static::$events); + $this->assertEquals(Operation::DELETE, static::$events[0]->getOperation()); + } + + public function testVersionedEvents(): void + { + /** @var Member $member */ + $member = $this->objFromFixture(Member::class, 'member1'); + Security::setCurrentUser($member); + + /** @var VersionedDataObject $object */ + $object = VersionedDataObject::create(['Title' => 'Test']); + $object->write(); + + EventLoop::run(); + static::$events = []; + + // Test publish + $object->publishRecursive(); + EventLoop::run(); + + $this->assertCount(2, static::$events, 'Expected 2 events, 1 for create and 1 for publish'); + $this->assertEquals(Operation::PUBLISH, static::$events[1]->getOperation()); + $this->assertEquals($member->ID, static::$events[1]->getMemberID()); + + // Test unpublish + static::$events = []; + $object->doUnpublish(); + EventLoop::run(); + + $this->assertCount(2, static::$events, 'Expected 2 events, 1 for deleting the live version and 1 for unpublish'); + $this->assertEquals(Operation::UNPUBLISH, static::$events[1]->getOperation()); + + // Test archive + static::$events = []; + $object->doArchive(); + EventLoop::run(); + + $this->assertCount(2, static::$events, 'Expected 2 events, 1 for deleting the draft version version and 1 for archive'); + $this->assertEquals(Operation::ARCHIVE, static::$events[1]->getOperation()); + } +} \ No newline at end of file diff --git a/tests/Extension/EventDispatchExtensionTest.yml b/tests/Extension/EventDispatchExtensionTest.yml new file mode 100644 index 0000000..874fe2e --- /dev/null +++ b/tests/Extension/EventDispatchExtensionTest.yml @@ -0,0 +1,5 @@ +SilverStripe\Security\Member: + member1: + FirstName: 'Test' + Surname: 'User' + Email: 'test@example.com' \ No newline at end of file diff --git a/tests/Mock/SimpleDataObject.php b/tests/Mock/SimpleDataObject.php new file mode 100644 index 0000000..ac713e3 --- /dev/null +++ b/tests/Mock/SimpleDataObject.php @@ -0,0 +1,23 @@ + 'Varchar', + ]; + + private static array $extensions = [ + EventDispatchExtension::class, + ]; +} \ No newline at end of file diff --git a/tests/Mock/VersionedDataObject.php b/tests/Mock/VersionedDataObject.php new file mode 100644 index 0000000..a5236dd --- /dev/null +++ b/tests/Mock/VersionedDataObject.php @@ -0,0 +1,26 @@ + 'Varchar', + ]; + + private static array $extensions = [ + EventDispatchExtension::class, + Versioned::class, + ]; +} \ No newline at end of file diff --git a/tests/Service/EventServiceTest.php b/tests/Service/EventServiceTest.php new file mode 100644 index 0000000..b7ee230 --- /dev/null +++ b/tests/Service/EventServiceTest.php @@ -0,0 +1,95 @@ +get(EventService::class); + + // Add test listener + $service->addListener(get_class($event), function ($event) { + $event->handled = true; + }); + + // Dispatch event + $result = $service->dispatch($event); + + EventLoop::run(); + + // Assert listener was called + $this->assertTrue($result->handled, 'Event listener should have been called'); + } + + public function testEventDispatchWithConfiguredListener(): void + { + // Create test event + $event = new class { + public bool $handled = false; + }; + // Configure listener via config + $eventClass = get_class($event); + EventService::config()->set('listeners', [ + $eventClass => [ + function($event) { + $event->handled = true; + } + ] + ]); + + // Get fresh service instance with config applied + $service = Injector::inst()->get(EventService::class); + + // Dispatch event + $result = $service->dispatch($event); + + EventLoop::run(); + + // Assert listener was called + $this->assertTrue($result->handled, 'Configured event listener should have been called'); + } + + public function testEventDispatchWithConfiguredLoader(): void + { + // Create test event + $event = new class { + public bool $handled = false; + }; + + // Create test loader + $loader = new TestListenerLoader(get_class($event)); + + // Configure loader via config + EventService::config()->set('loaders', [$loader]); + + // Get fresh service instance with config applied + $service = Injector::inst()->get(EventService::class); + $this->assertTrue($loader->loaded, 'Loader should have been used'); + + // Dispatch event + $result = $service->dispatch($event); + + EventLoop::run(); + + // Assert loader was used and listener was called + $this->assertTrue($loader->eventFired, 'Configured event listener should have been called'); + } +} \ No newline at end of file diff --git a/tests/TestListenerLoader.php b/tests/TestListenerLoader.php new file mode 100644 index 0000000..77c87ae --- /dev/null +++ b/tests/TestListenerLoader.php @@ -0,0 +1,31 @@ +loaded = true; + $provider->addListener($this->eventName, [$this, 'handleEvent']); + } + + public function handleEvent(object $event): void + { + $this->eventFired = true; + } +}