From 30d6d46063e205e38a413ce82bd64582830002a4 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Thu, 7 Nov 2024 10:48:30 +1300 Subject: [PATCH 01/26] Initial commit --- .github/workflows/ci.yml | 47 +++++ .gitignore | 9 + .php-cs-fixer.dist.php | 32 +++ README.md | 5 + _config/events.yml | 10 + composer.json | 40 ++++ phpstan.neon | 14 ++ src/Event/AbstractDataObjectEvent.php | 52 +++++ src/Event/DataObjectDeleteEvent.php | 13 ++ src/Event/DataObjectEvent.php | 27 +++ src/Event/DataObjectVersionEvent.php | 37 ++++ src/Event/DataObjectWriteEvent.php | 16 ++ src/Extension/EventDispatchExtension.php | 211 ++++++++++++++++++++ src/Service/EventService.php | 60 ++++++ tests/Event/AbstractDataObjectEventTest.php | 43 ++++ tests/Event/DataObjectVersionEventTest.php | 42 ++++ 16 files changed, 658 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.dist.php create mode 100644 README.md create mode 100644 _config/events.yml create mode 100644 composer.json create mode 100644 phpstan.neon create mode 100644 src/Event/AbstractDataObjectEvent.php create mode 100644 src/Event/DataObjectDeleteEvent.php create mode 100644 src/Event/DataObjectEvent.php create mode 100644 src/Event/DataObjectVersionEvent.php create mode 100644 src/Event/DataObjectWriteEvent.php create mode 100644 src/Extension/EventDispatchExtension.php create mode 100644 src/Service/EventService.php create mode 100644 tests/Event/AbstractDataObjectEventTest.php create mode 100644 tests/Event/DataObjectVersionEventTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ed8bce1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + tests: + name: PHP ${{ matrix.php }} - Silverstripe ${{ matrix.silverstripe }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.1'] + silverstripe: ['4.13', '5.0'] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, intl, pdo, mysql + coverage: xdebug + + - name: Install dependencies + run: | + composer require --no-update silverstripe/framework:^${{ matrix.silverstripe }} + composer install --no-interaction --no-progress + + - name: Run PHP CS Fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff + + - name: Run PHPStan + run: vendor/bin/phpstan analyse + + - name: Run PHPUnit + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ffc7fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/vendor/ +.phpunit.result.cache +.php-cs-cache +.env +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..a9aeee2 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,32 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +$config = new PhpCsFixer\Config(); + +return $config + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_align' => true, + 'phpdoc_order' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_trim' => true, + 'phpdoc_var_without_name' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'single_quote' => true, + 'ternary_operator_spaces' => true, + 'unary_operator_spaces' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da6e031 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Silverstripe Events Module + +This module provides PSR-14 Event Dispatcher integration for Silverstripe CMS with automatic event dispatching for DataObject CRUD operations and versioning actions. + +## Installation \ No newline at end of file diff --git a/_config/events.yml b/_config/events.yml new file mode 100644 index 0000000..8a64c6e --- /dev/null +++ b/_config/events.yml @@ -0,0 +1,10 @@ +--- +Name: events +After: + - '#coreservices' +--- +SilverStripe\Core\Injector\Injector: + ArchiPro\Silverstripe\EventDispatcher\Service\EventService: + constructor: + dispatcher: '%$Psr\EventDispatcher\EventDispatcherInterface' + listenerProvider: '%$Psr\EventDispatcher\ListenerProviderInterface' \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..446aad3 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "archipro/silverstripe-event-dispatcher", + "description": "PSR-14 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" + }, + "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" + }, + "autoload": { + "psr-4": { + "ArchiPro\\Silverstripe\\EventDispatcher\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ArchiPro\\Silverstripe\\EventDispatcher\\Tests\\": "tests/" + } + }, + "scripts": { + "lint": "php-cs-fixer fix --dry-run --diff", + "lint-fix": "php-cs-fixer fix", + "analyse": "phpstan analyse", + "test": "phpunit" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "extra": { + "expose": [] + } +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..652fa71 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,14 @@ +parameters: + level: 8 + paths: + - src + - tests + excludePaths: + - vendor/* + silverstripe: + checkUnusedViewVariables: false + ignoreErrors: + - '#Access to an undefined property .+::\$owner#' + - '#Call to an undefined method .+::hasExtension\(\)#' +includes: + - vendor/symbiote/silverstripe-phpstan/phpstan.neon \ No newline at end of file diff --git a/src/Event/AbstractDataObjectEvent.php b/src/Event/AbstractDataObjectEvent.php new file mode 100644 index 0000000..0e989e6 --- /dev/null +++ b/src/Event/AbstractDataObjectEvent.php @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..258267f --- /dev/null +++ b/src/Event/DataObjectDeleteEvent.php @@ -0,0 +1,13 @@ +dataObject = $dataObject; + $this->action = $action; + } + + public function getDataObject(): DataObject + { + return $this->dataObject; + } + + public function getAction(): string + { + return $this->action; + } +} \ No newline at end of file diff --git a/src/Event/DataObjectVersionEvent.php b/src/Event/DataObjectVersionEvent.php new file mode 100644 index 0000000..211a2e1 --- /dev/null +++ b/src/Event/DataObjectVersionEvent.php @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..1609a5f --- /dev/null +++ b/src/Event/DataObjectWriteEvent.php @@ -0,0 +1,16 @@ +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() + ); + + $this->dispatchEvent($event); + } + + /** + * 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, + ] + ); + + $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)) { + return; + } + + $event = new DataObjectVersionEvent( + $this->owner->ID, + get_class($this->owner), + 'publish', + $this->getChanges(), + $this->owner->Version + ); + + $this->dispatchEvent($event); + } + + /** + * Fires when a versioned DataObject is unpublished + */ + public function onAfterUnpublish(): void + { + if (!$this->owner->hasExtension(Versioned::class)) { + return; + } + + $event = new DataObjectVersionEvent( + $this->owner->ID, + get_class($this->owner), + 'unpublish', + [], + $this->owner->Version + ); + + $this->dispatchEvent($event); + } + + /** + * Fires when a versioned DataObject is archived + */ + public function onAfterArchive(): void + { + if (!$this->owner->hasExtension(Versioned::class)) { + return; + } + + $event = new DataObjectVersionEvent( + $this->owner->ID, + get_class($this->owner), + 'archive', + [], + $this->owner->Version + ); + + $this->dispatchEvent($event); + } + + /** + * Fires when a versioned DataObject is restored from archive + */ + public function onAfterRestore(): void + { + if (!$this->owner->hasExtension(Versioned::class)) { + return; + } + + $event = new DataObjectVersionEvent( + $this->owner->ID, + get_class($this->owner), + 'restore', + [], + $this->owner->Version + ); + + $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 + { + return Injector::inst()->get(EventService::class)->dispatch($event); + } +} \ No newline at end of file diff --git a/src/Service/EventService.php b/src/Service/EventService.php new file mode 100644 index 0000000..9dae391 --- /dev/null +++ b/src/Service/EventService.php @@ -0,0 +1,60 @@ +dispatcher = $dispatcher; + $this->listenerProvider = $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 + { + return $this->dispatcher->dispatch($event); + } + + /** + * Gets the listener provider instance + * + * @return ListenerProviderInterface + */ + public function getListenerProvider(): ListenerProviderInterface + { + return $this->listenerProvider; + } +} \ No newline at end of file diff --git a/tests/Event/AbstractDataObjectEventTest.php b/tests/Event/AbstractDataObjectEventTest.php new file mode 100644 index 0000000..7b297de --- /dev/null +++ b/tests/Event/AbstractDataObjectEventTest.php @@ -0,0 +1,43 @@ + ['old' => null, 'new' => 'New Page']] + ); + + $this->assertEquals(1, $event->getObjectID()); + $this->assertEquals('Page', $event->getObjectClass()); + $this->assertEquals('create', $event->getAction()); + $this->assertArrayHasKey('Title', $event->getChanges()); + } + + public function testJsonSerialization(): void + { + $event = new DataObjectWriteEvent( + 1, + 'Page', + 'create', + ['Title' => ['old' => null, 'new' => 'New Page']] + ); + + $json = json_encode($event); + $data = json_decode($json, true); + + $this->assertArrayHasKey('id', $data); + $this->assertArrayHasKey('class', $data); + $this->assertArrayHasKey('action', $data); + $this->assertArrayHasKey('changes', $data); + $this->assertArrayHasKey('timestamp', $data); + } +} \ No newline at end of file diff --git a/tests/Event/DataObjectVersionEventTest.php b/tests/Event/DataObjectVersionEventTest.php new file mode 100644 index 0000000..c50f971 --- /dev/null +++ b/tests/Event/DataObjectVersionEventTest.php @@ -0,0 +1,42 @@ + ['old' => 'Old Title', 'new' => 'New Title']] + ); + + $this->assertEquals(1, $event->getObjectID()); + $this->assertEquals('Page', $event->getObjectClass()); + $this->assertEquals('publish', $event->getAction()); + $this->assertEquals(2, $event->getVersion()); + } + + public function testVersionJsonSerialization(): void + { + $event = new DataObjectVersionEvent( + 1, + 'Page', + 'publish', + 2, + [] + ); + + $json = json_encode($event); + $data = json_decode($json, true); + + $this->assertArrayHasKey('version', $data); + $this->assertEquals(2, $data['version']); + } +} \ No newline at end of file From 4911860b845484bf0e8a28c1747a451eb0fd071a Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Thu, 7 Nov 2024 10:50:50 +1300 Subject: [PATCH 02/26] Update workfow --- .github/workflows/ci.yml | 59 +++++++++++++--------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed8bce1..713ab2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,46 +2,25 @@ name: CI on: push: - branches: [ main ] pull_request: - branches: [ main ] + workflow_dispatch: jobs: - tests: - name: PHP ${{ matrix.php }} - Silverstripe ${{ matrix.silverstripe }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php: ['8.1'] - silverstripe: ['4.13', '5.0'] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: mbstring, intl, pdo, mysql - coverage: xdebug - - - name: Install dependencies - run: | - composer require --no-update silverstripe/framework:^${{ matrix.silverstripe }} - composer install --no-interaction --no-progress - - - name: Run PHP CS Fixer - run: vendor/bin/php-cs-fixer fix --dry-run --diff - - - name: Run PHPStan - run: vendor/bin/phpstan analyse - - - name: Run PHPUnit - run: vendor/bin/phpunit --coverage-clover=coverage.xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml \ No newline at end of file + ci: + name: CI + uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 + with: + dynamic_matrix: true + # extra_jobs: | + # - php: '8.1' + # db: mysql80 + # phpunit: true + # installer_version: ^4 + # - php: '8.2' + # db: mysql80 + # phpunit: true + # installer_version: ^5 + # - php: '8.2' + # db: mariadb + # phpunit: true + # installer_version: ^5 \ No newline at end of file From 5de5068ad93a999ea4b065579ff35e5442d1e953 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Thu, 7 Nov 2024 16:50:18 +1300 Subject: [PATCH 03/26] Add ability to scaffold Dataobject event --- .gitignore | 4 +- _config/events.yml | 19 +- composer.json | 27 ++- src/Contract/ListenerLoaderInterface.php | 25 +++ src/DataObjectEventListener.php | 75 ++++++++ src/Event/AbstractDataObjectEvent.php | 52 ------ src/Event/DataObjectDeleteEvent.php | 13 -- src/Event/DataObjectEvent.php | 175 +++++++++++++++++- src/Event/DataObjectVersionEvent.php | 37 ---- src/Event/DataObjectWriteEvent.php | 16 -- src/Event/Operation.php | 28 +++ src/Extension/EventDispatchExtension.php | 172 ++++++----------- src/Service/EventService.php | 93 +++++++--- tests/Event/DataObjectEventTest.php | 96 ++++++++++ tests/Event/DataObjectEventTest.yml | 13 ++ .../Extension/EventDispatchExtensionTest.php | 125 +++++++++++++ .../Extension/EventDispatchExtensionTest.yml | 5 + tests/Mock/SimpleDataObject.php | 23 +++ tests/Mock/VersionedDataObject.php | 26 +++ tests/Service/EventServiceTest.php | 95 ++++++++++ tests/TestListenerLoader.php | 31 ++++ 21 files changed, 871 insertions(+), 279 deletions(-) create mode 100644 src/Contract/ListenerLoaderInterface.php create mode 100644 src/DataObjectEventListener.php delete mode 100644 src/Event/AbstractDataObjectEvent.php delete mode 100644 src/Event/DataObjectDeleteEvent.php delete mode 100644 src/Event/DataObjectVersionEvent.php delete mode 100644 src/Event/DataObjectWriteEvent.php create mode 100644 src/Event/Operation.php create mode 100644 tests/Event/DataObjectEventTest.php create mode 100644 tests/Event/DataObjectEventTest.yml create mode 100644 tests/Extension/EventDispatchExtensionTest.php create mode 100644 tests/Extension/EventDispatchExtensionTest.yml create mode 100644 tests/Mock/SimpleDataObject.php create mode 100644 tests/Mock/VersionedDataObject.php create mode 100644 tests/Service/EventServiceTest.php create mode 100644 tests/TestListenerLoader.php 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; + } +} From 3095867b25e4194bd6909d25ee2923c1601519da Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 09:57:22 +1300 Subject: [PATCH 04/26] Update CI workflow --- .github/workflows/ci.yml | 65 +++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 713ab2a..eb97501 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,25 +2,54 @@ name: CI on: push: + branches: [ master ] pull_request: + branches: [ master ] workflow_dispatch: jobs: - ci: - name: CI - uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 - with: - dynamic_matrix: true - # extra_jobs: | - # - php: '8.1' - # db: mysql80 - # phpunit: true - # installer_version: ^4 - # - php: '8.2' - # db: mysql80 - # phpunit: true - # installer_version: ^5 - # - php: '8.2' - # db: mariadb - # phpunit: true - # installer_version: ^5 \ No newline at end of file + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.1', '8.2', '8.3'] + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit + + - name: Static Analysis + run: vendor/bin/phpstan analyse + + coding-standards: + name: Coding Standards + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + tools: composer:v2, php-cs-fixer + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Check coding standards + run: php-cs-fixer fix --dry-run --diff \ No newline at end of file From dcf32e8c003fe60f821b0ae7545d88653c688f97 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 10:02:23 +1300 Subject: [PATCH 05/26] Add PHPUnit config --- phpunit.xml.dist | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 phpunit.xml.dist diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..3801dc3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + tests/php + + + From 263401f9465f04f1d8786cf325f6de213e454777 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 10:05:22 +1300 Subject: [PATCH 06/26] Move test around --- .github/workflows/ci.yml | 45 ++++++++----------- composer.json | 2 +- .../Event/AbstractDataObjectEventTest.php | 0 tests/{ => php}/Event/DataObjectEventTest.php | 0 tests/{ => php}/Event/DataObjectEventTest.yml | 0 .../Event/DataObjectVersionEventTest.php | 0 .../Extension/EventDispatchExtensionTest.php | 0 .../Extension/EventDispatchExtensionTest.yml | 0 tests/{ => php}/Mock/SimpleDataObject.php | 0 tests/{ => php}/Mock/VersionedDataObject.php | 0 tests/{ => php}/Service/EventServiceTest.php | 0 tests/{ => php}/TestListenerLoader.php | 0 12 files changed, 19 insertions(+), 28 deletions(-) rename tests/{ => php}/Event/AbstractDataObjectEventTest.php (100%) rename tests/{ => php}/Event/DataObjectEventTest.php (100%) rename tests/{ => php}/Event/DataObjectEventTest.yml (100%) rename tests/{ => php}/Event/DataObjectVersionEventTest.php (100%) rename tests/{ => php}/Extension/EventDispatchExtensionTest.php (100%) rename tests/{ => php}/Extension/EventDispatchExtensionTest.yml (100%) rename tests/{ => php}/Mock/SimpleDataObject.php (100%) rename tests/{ => php}/Mock/VersionedDataObject.php (100%) rename tests/{ => php}/Service/EventServiceTest.php (100%) rename tests/{ => php}/TestListenerLoader.php (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb97501..3b2065e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,37 +2,28 @@ name: CI on: push: - branches: [ master ] pull_request: - branches: [ master ] workflow_dispatch: jobs: - tests: - name: PHP ${{ matrix.php }} - runs-on: ubuntu-latest - strategy: - matrix: - php: ['8.1', '8.2', '8.3'] - - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: none - tools: composer:v2 - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run tests - run: vendor/bin/phpunit - - - name: Static Analysis - run: vendor/bin/phpstan analyse + ci: + name: CI + uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 + with: + dynamic_matrix: false + extra_jobs: | + - php: '8.1' + db: mysql80 + phpunit: true + installer_version: ^4 + - php: '8.2' + db: mysql80 + phpunit: true + installer_version: ^5 + - php: '8.2' + db: mariadb + phpunit: true + installer_version: ^5 coding-standards: name: Coding Standards diff --git a/composer.json b/composer.json index d92c087..be00039 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ }, "autoload-dev": { "psr-4": { - "ArchiPro\\Silverstripe\\EventDispatcher\\Tests\\": "tests/" + "ArchiPro\\Silverstripe\\EventDispatcher\\Tests\\": "tests/php/" } }, "scripts": { diff --git a/tests/Event/AbstractDataObjectEventTest.php b/tests/php/Event/AbstractDataObjectEventTest.php similarity index 100% rename from tests/Event/AbstractDataObjectEventTest.php rename to tests/php/Event/AbstractDataObjectEventTest.php diff --git a/tests/Event/DataObjectEventTest.php b/tests/php/Event/DataObjectEventTest.php similarity index 100% rename from tests/Event/DataObjectEventTest.php rename to tests/php/Event/DataObjectEventTest.php diff --git a/tests/Event/DataObjectEventTest.yml b/tests/php/Event/DataObjectEventTest.yml similarity index 100% rename from tests/Event/DataObjectEventTest.yml rename to tests/php/Event/DataObjectEventTest.yml diff --git a/tests/Event/DataObjectVersionEventTest.php b/tests/php/Event/DataObjectVersionEventTest.php similarity index 100% rename from tests/Event/DataObjectVersionEventTest.php rename to tests/php/Event/DataObjectVersionEventTest.php diff --git a/tests/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php similarity index 100% rename from tests/Extension/EventDispatchExtensionTest.php rename to tests/php/Extension/EventDispatchExtensionTest.php diff --git a/tests/Extension/EventDispatchExtensionTest.yml b/tests/php/Extension/EventDispatchExtensionTest.yml similarity index 100% rename from tests/Extension/EventDispatchExtensionTest.yml rename to tests/php/Extension/EventDispatchExtensionTest.yml diff --git a/tests/Mock/SimpleDataObject.php b/tests/php/Mock/SimpleDataObject.php similarity index 100% rename from tests/Mock/SimpleDataObject.php rename to tests/php/Mock/SimpleDataObject.php diff --git a/tests/Mock/VersionedDataObject.php b/tests/php/Mock/VersionedDataObject.php similarity index 100% rename from tests/Mock/VersionedDataObject.php rename to tests/php/Mock/VersionedDataObject.php diff --git a/tests/Service/EventServiceTest.php b/tests/php/Service/EventServiceTest.php similarity index 100% rename from tests/Service/EventServiceTest.php rename to tests/php/Service/EventServiceTest.php diff --git a/tests/TestListenerLoader.php b/tests/php/TestListenerLoader.php similarity index 100% rename from tests/TestListenerLoader.php rename to tests/php/TestListenerLoader.php From 86c7bb96fffb338e8bb295ac9deafcb660dad3bf Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 10:07:05 +1300 Subject: [PATCH 07/26] Test PHP 8.3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b2065e..eb42d42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: db: mysql80 phpunit: true installer_version: ^5 - - php: '8.2' + - php: '8.3' db: mariadb phpunit: true installer_version: ^5 From abcaf6db0b9f407cdc71b998c09a67a3b2a6cf02 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 10:08:08 +1300 Subject: [PATCH 08/26] Fix PHP linting error --- .gitignore | 3 +- src/Contract/ListenerLoaderInterface.php | 8 ++--- src/DataObjectEventListener.php | 13 +++---- src/Event/DataObjectEvent.php | 36 +++++++++---------- src/Event/Operation.php | 6 ++-- src/Extension/EventDispatchExtension.php | 23 ++++++------ src/Service/EventService.php | 9 +++-- .../php/Event/AbstractDataObjectEventTest.php | 4 +-- tests/php/Event/DataObjectEventTest.php | 25 +++++++------ .../php/Event/DataObjectVersionEventTest.php | 4 +-- .../Extension/EventDispatchExtensionTest.php | 10 +++--- tests/php/Mock/SimpleDataObject.php | 2 +- tests/php/Mock/VersionedDataObject.php | 3 +- tests/php/Service/EventServiceTest.php | 26 +++++++------- tests/php/TestListenerLoader.php | 3 +- 15 files changed, 91 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index 325dd6f..4a59061 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ *.swo .DS_Store composer.lock -/public/ \ No newline at end of file +/public/ +.php-cs-fixer.cache \ No newline at end of file diff --git a/src/Contract/ListenerLoaderInterface.php b/src/Contract/ListenerLoaderInterface.php index bde31fb..413ff80 100644 --- a/src/Contract/ListenerLoaderInterface.php +++ b/src/Contract/ListenerLoaderInterface.php @@ -6,7 +6,7 @@ /** * Interface for classes that load event listeners into a ListenerProvider. - * + * * This interface allows for modular and configurable loading of event listeners, * making it easier to organize and maintain event listeners in different parts * of the application. @@ -15,11 +15,11 @@ interface ListenerLoaderInterface { /** * Loads event listeners into the provided ListenerProvider. - * + * * Implementations should use this method to register their event listeners * with the provider, typically using the provider's addListener method. - * + * * @param ListenerProvider $provider The provider to load listeners into */ public function loadListeners(ListenerProvider $provider): void; -} \ No newline at end of file +} diff --git a/src/DataObjectEventListener.php b/src/DataObjectEventListener.php index 91ab094..a12feba 100644 --- a/src/DataObjectEventListener.php +++ b/src/DataObjectEventListener.php @@ -9,7 +9,7 @@ /** * Event listener for DataObject events that filters events based on operation type and object class. - * + * * This listener can be configured to only handle specific operations (create, update, delete etc) * and specific DataObject classes. When an event matches the configured criteria, the callback * is executed with the event. @@ -19,9 +19,9 @@ class DataObjectEventListener /** * Creates a new DataObject event listener. * - * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches - * @param class-string[] $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. + * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches + * @param class-string[] $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, @@ -33,7 +33,7 @@ public function __construct( /** * Handles a DataObject event. - * + * * Checks if the event matches the configured operations and classes, * and executes the callback if it does. * @@ -57,10 +57,11 @@ public function __invoke(DataObjectEvent $event): void /** * 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 diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index 47ff3d4..b2cf07e 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -9,18 +9,18 @@ /** * Event class representing operations performed on DataObjects. - * + * * This event is dispatched whenever a significant operation occurs on a DataObject, * such as creation, updates, deletion, or versioning operations. It captures key * information about the operation including: - * + * * - The ID of the affected DataObject * - The class of the DataObject * - The type of operation performed * - The version number (for versioned objects) * - The ID of the member who performed the operation * - The timestamp when the operation occurred - * + * * Example usage: * ```php * $event = DataObjectEvent::create( @@ -42,11 +42,11 @@ class DataObjectEvent 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 + * @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, @@ -108,9 +108,9 @@ public function getTimestamp(): int /** * 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 + * Note: This may return null if the object has been deleted since the event was created */ public function getObject(bool $useVersion = false): ?DataObject { @@ -119,12 +119,12 @@ public function getObject(bool $useVersion = false): ?DataObject } $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 + return $object->Version == $this->version + ? $object : $object->Versions()->byID($this->version); } @@ -133,7 +133,7 @@ public function getObject(bool $useVersion = false): ?DataObject /** * 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 */ @@ -163,20 +163,20 @@ public function serialize(): string /** * Unserialize the event from a string - * + * * @param string $data */ public function unserialize(string $data): void { $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/Operation.php b/src/Event/Operation.php index 679854c..b0975a8 100644 --- a/src/Event/Operation.php +++ b/src/Event/Operation.php @@ -4,10 +4,10 @@ /** * Represents the type of operation performed on a DataObject. - * + * * This enum is used to identify what kind of operation triggered a DataObjectEvent. * Each operation maps to a specific action in the Silverstripe CMS: - * + * * - CREATE: First time a DataObject is written to the database * - UPDATE: Subsequent writes to an existing DataObject * - DELETE: When a DataObject is deleted (both soft and hard deletes) @@ -25,4 +25,4 @@ enum Operation: string case UNPUBLISH = 'unpublish'; case ARCHIVE = 'archive'; case RESTORE = 'restore'; -} \ No newline at end of file +} diff --git a/src/Extension/EventDispatchExtension.php b/src/Extension/EventDispatchExtension.php index 79fbb55..6c35325 100644 --- a/src/Extension/EventDispatchExtension.php +++ b/src/Extension/EventDispatchExtension.php @@ -2,19 +2,20 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Extension; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\ORM\DataObject; -use SilverStripe\Versioned\Versioned; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectEvent; use ArchiPro\Silverstripe\EventDispatcher\Event\Operation; use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; use SilverStripe\Core\Extension; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\ORM\DataObject; use SilverStripe\Security\Security; +use SilverStripe\Versioned\Versioned; /** * Extension that adds event dispatching capabilities to DataObjects. - * + * * @property DataObject|Versioned $owner + * * @method DataObject getOwner() */ class EventDispatchExtension extends Extension @@ -34,7 +35,7 @@ public function onAfterWrite(): void $owner->hasExtension(Versioned::class) ? $owner->Version : null, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -51,7 +52,7 @@ public function onBeforeDelete(): void $owner->hasExtension(Versioned::class) ? $owner->Version : null, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -72,7 +73,7 @@ public function onAfterPublish(): void $owner->Version, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -93,7 +94,7 @@ public function onAfterUnpublish(): void $owner->Version, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -114,7 +115,7 @@ public function onAfterArchive(): void $owner->Version, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -135,7 +136,7 @@ public function onAfterRestore(): void $owner->Version, Security::getCurrentUser()?->ID ); - + $this->dispatchEvent($event); } @@ -146,4 +147,4 @@ protected function dispatchEvent(DataObjectEvent $event): DataObjectEvent { return Injector::inst()->get(EventService::class)->dispatch($event); } -} \ No newline at end of file +} diff --git a/src/Service/EventService.php b/src/Service/EventService.php index 9fb133b..b511604 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -5,12 +5,12 @@ use ArchiPro\EventDispatcher\AsyncEventDispatcher; use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\Silverstripe\EventDispatcher\Contract\ListenerLoaderInterface; -use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Config\Configurable; +use SilverStripe\Core\Injector\Injectable; /** * 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. */ @@ -21,12 +21,14 @@ class EventService /** * @config + * * @var array> Map of event class names to arrays of listener callbacks */ private static array $listeners = []; /** * @config + * * @var array Array of listener loaders */ private static array $loaders = []; @@ -76,6 +78,7 @@ public function addListener(string $event, callable $listener): void /** * Adds a listener loader to the event service + * * @throws \RuntimeException If the loader does not implement ListenerLoaderInterface */ public function addListenerLoader(ListenerLoaderInterface $loader): void @@ -104,4 +107,4 @@ public function getListenerProvider(): ListenerProvider { return $this->listenerProvider; } -} \ No newline at end of file +} diff --git a/tests/php/Event/AbstractDataObjectEventTest.php b/tests/php/Event/AbstractDataObjectEventTest.php index 7b297de..47ec31a 100644 --- a/tests/php/Event/AbstractDataObjectEventTest.php +++ b/tests/php/Event/AbstractDataObjectEventTest.php @@ -2,8 +2,8 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Tests\Event; -use SilverStripe\Dev\SapphireTest; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectWriteEvent; +use SilverStripe\Dev\SapphireTest; class AbstractDataObjectEventTest extends SapphireTest { @@ -40,4 +40,4 @@ public function testJsonSerialization(): void $this->assertArrayHasKey('changes', $data); $this->assertArrayHasKey('timestamp', $data); } -} \ No newline at end of file +} diff --git a/tests/php/Event/DataObjectEventTest.php b/tests/php/Event/DataObjectEventTest.php index ae6ffb7..31e47ed 100644 --- a/tests/php/Event/DataObjectEventTest.php +++ b/tests/php/Event/DataObjectEventTest.php @@ -8,7 +8,6 @@ use ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\VersionedDataObject; use SilverStripe\Dev\SapphireTest; use SilverStripe\Security\Member; -use SilverStripe\Versioned\Versioned; class DataObjectEventTest extends SapphireTest { @@ -35,9 +34,9 @@ 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); } @@ -46,21 +45,21 @@ 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); @@ -71,9 +70,9 @@ 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); } @@ -81,11 +80,11 @@ public function testGetMember(): void 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()); @@ -93,4 +92,4 @@ public function testSerialization(): void $this->assertEquals(3, $unserialized->getMemberID()); $this->assertEquals($event->getTimestamp(), $unserialized->getTimestamp()); } -} \ No newline at end of file +} diff --git a/tests/php/Event/DataObjectVersionEventTest.php b/tests/php/Event/DataObjectVersionEventTest.php index c50f971..c4a2eed 100644 --- a/tests/php/Event/DataObjectVersionEventTest.php +++ b/tests/php/Event/DataObjectVersionEventTest.php @@ -2,8 +2,8 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Tests\Event; -use SilverStripe\Dev\SapphireTest; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectVersionEvent; +use SilverStripe\Dev\SapphireTest; class DataObjectVersionEventTest extends SapphireTest { @@ -39,4 +39,4 @@ public function testVersionJsonSerialization(): void $this->assertArrayHasKey('version', $data); $this->assertEquals(2, $data['version']); } -} \ No newline at end of file +} diff --git a/tests/php/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php index a0088c8..474b988 100644 --- a/tests/php/Extension/EventDispatchExtensionTest.php +++ b/tests/php/Extension/EventDispatchExtensionTest.php @@ -76,7 +76,7 @@ public function testDeleteEvent(): void $object = SimpleDataObject::create(['Title' => 'Test']); $object->write(); EventLoop::run(); - + static::$events = []; $object->delete(); EventLoop::run(); @@ -101,7 +101,7 @@ public function testVersionedEvents(): void // 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()); @@ -110,7 +110,7 @@ public function testVersionedEvents(): void 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()); @@ -118,8 +118,8 @@ public function testVersionedEvents(): void 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/php/Mock/SimpleDataObject.php b/tests/php/Mock/SimpleDataObject.php index ac713e3..827d00f 100644 --- a/tests/php/Mock/SimpleDataObject.php +++ b/tests/php/Mock/SimpleDataObject.php @@ -20,4 +20,4 @@ class SimpleDataObject extends DataObject implements TestOnly private static array $extensions = [ EventDispatchExtension::class, ]; -} \ No newline at end of file +} diff --git a/tests/php/Mock/VersionedDataObject.php b/tests/php/Mock/VersionedDataObject.php index a5236dd..dc3030f 100644 --- a/tests/php/Mock/VersionedDataObject.php +++ b/tests/php/Mock/VersionedDataObject.php @@ -9,6 +9,7 @@ /** * @property string $Title + * * @mixin Versioned */ class VersionedDataObject extends DataObject implements TestOnly @@ -23,4 +24,4 @@ class VersionedDataObject extends DataObject implements TestOnly EventDispatchExtension::class, Versioned::class, ]; -} \ No newline at end of file +} diff --git a/tests/php/Service/EventServiceTest.php b/tests/php/Service/EventServiceTest.php index b7ee230..4fe9416 100644 --- a/tests/php/Service/EventServiceTest.php +++ b/tests/php/Service/EventServiceTest.php @@ -5,20 +5,20 @@ use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; use ArchiPro\Silverstripe\EventDispatcher\Tests\TestListenerLoader; use Revolt\EventLoop; -use SilverStripe\Dev\SapphireTest; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Dev\SapphireTest; + class EventServiceTest extends SapphireTest { - protected function setUp(): void { parent::setUp(); } - + public function testEventDispatch(): void { // Create test event - $event = new class { + $event = new class () { public bool $handled = false; }; @@ -42,17 +42,17 @@ public function testEventDispatch(): void public function testEventDispatchWithConfiguredListener(): void { // Create test event - $event = new class { + $event = new class () { public bool $handled = false; }; // Configure listener via config $eventClass = get_class($event); EventService::config()->set('listeners', [ $eventClass => [ - function($event) { + function ($event) { $event->handled = true; - } - ] + }, + ], ]); // Get fresh service instance with config applied @@ -70,10 +70,10 @@ function($event) { public function testEventDispatchWithConfiguredLoader(): void { // Create test event - $event = new class { + $event = new class () { public bool $handled = false; }; - + // Create test loader $loader = new TestListenerLoader(get_class($event)); @@ -83,13 +83,13 @@ public function testEventDispatchWithConfiguredLoader(): void // 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/php/TestListenerLoader.php b/tests/php/TestListenerLoader.php index 77c87ae..c8e6301 100644 --- a/tests/php/TestListenerLoader.php +++ b/tests/php/TestListenerLoader.php @@ -16,7 +16,8 @@ class TestListenerLoader implements ListenerLoaderInterface public function __construct( private string $eventName - ) {} + ) { + } public function loadListeners(ListenerProvider $provider): void { From bdf4dfac76056c8a24b31a8d4c219cb5af7f7227 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 11:00:00 +1300 Subject: [PATCH 09/26] Finishing implementing test for DataObjectEvent flow --- src/DataObjectEventListener.php | 19 ++++++++ src/Event/DataObjectEvent.php | 13 ++---- .../php/Event/AbstractDataObjectEventTest.php | 43 ------------------- .../php/Event/DataObjectVersionEventTest.php | 42 ------------------ .../Extension/EventDispatchExtensionTest.php | 7 +-- 5 files changed, 25 insertions(+), 99 deletions(-) delete mode 100644 tests/php/Event/AbstractDataObjectEventTest.php delete mode 100644 tests/php/Event/DataObjectVersionEventTest.php diff --git a/src/DataObjectEventListener.php b/src/DataObjectEventListener.php index a12feba..b15a1f0 100644 --- a/src/DataObjectEventListener.php +++ b/src/DataObjectEventListener.php @@ -2,9 +2,13 @@ namespace ArchiPro\Silverstripe\EventDispatcher; +use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectEvent; use ArchiPro\Silverstripe\EventDispatcher\Event\Operation; +use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; use Closure; +use SilverStripe\Core\Injector\Injectable; +use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DataObject; /** @@ -16,6 +20,8 @@ */ class DataObjectEventListener { + use Injectable; + /** * Creates a new DataObject event listener. * @@ -31,6 +37,19 @@ public function __construct( $this->operations = $operations ?? Operation::cases(); } + /** + * Registers this listener with the given provider. + * + * If no provider is provided, the global EventService will be used. + */ + public function selfRegister(ListenerProvider|EventService $provider = null): void + { + if (empty($provider)) { + $provider = Injector::inst()->get(EventService::class); + } + $provider->addListener(DataObjectEvent::class, $this); + } + /** * Handles a DataObject event. * diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index b2cf07e..29d562d 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -2,6 +2,7 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Event; +use SilverStripe\Core\Extension; use SilverStripe\Core\Injector\Injectable; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; @@ -118,17 +119,11 @@ public function getObject(bool $useVersion = false): ?DataObject 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); + if (!$useVersion || empty($this->version)) { + return DataObject::get_by_id($this->objectClass, $this->objectID, false); } - return $object; + return Versioned::get_version($this->objectClass, $this->objectID, $this->version); } /** diff --git a/tests/php/Event/AbstractDataObjectEventTest.php b/tests/php/Event/AbstractDataObjectEventTest.php deleted file mode 100644 index 47ec31a..0000000 --- a/tests/php/Event/AbstractDataObjectEventTest.php +++ /dev/null @@ -1,43 +0,0 @@ - ['old' => null, 'new' => 'New Page']] - ); - - $this->assertEquals(1, $event->getObjectID()); - $this->assertEquals('Page', $event->getObjectClass()); - $this->assertEquals('create', $event->getAction()); - $this->assertArrayHasKey('Title', $event->getChanges()); - } - - public function testJsonSerialization(): void - { - $event = new DataObjectWriteEvent( - 1, - 'Page', - 'create', - ['Title' => ['old' => null, 'new' => 'New Page']] - ); - - $json = json_encode($event); - $data = json_decode($json, true); - - $this->assertArrayHasKey('id', $data); - $this->assertArrayHasKey('class', $data); - $this->assertArrayHasKey('action', $data); - $this->assertArrayHasKey('changes', $data); - $this->assertArrayHasKey('timestamp', $data); - } -} diff --git a/tests/php/Event/DataObjectVersionEventTest.php b/tests/php/Event/DataObjectVersionEventTest.php deleted file mode 100644 index c4a2eed..0000000 --- a/tests/php/Event/DataObjectVersionEventTest.php +++ /dev/null @@ -1,42 +0,0 @@ - ['old' => 'Old Title', 'new' => 'New Title']] - ); - - $this->assertEquals(1, $event->getObjectID()); - $this->assertEquals('Page', $event->getObjectClass()); - $this->assertEquals('publish', $event->getAction()); - $this->assertEquals(2, $event->getVersion()); - } - - public function testVersionJsonSerialization(): void - { - $event = new DataObjectVersionEvent( - 1, - 'Page', - 'publish', - 2, - [] - ); - - $json = json_encode($event); - $data = json_decode($json, true); - - $this->assertArrayHasKey('version', $data); - $this->assertEquals(2, $data['version']); - } -} diff --git a/tests/php/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php index 474b988..13cc8f2 100644 --- a/tests/php/Extension/EventDispatchExtensionTest.php +++ b/tests/php/Extension/EventDispatchExtensionTest.php @@ -30,15 +30,12 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - $service = Injector::inst()->get(EventService::class); - - // Add listener that captures events - $service->addListener(DataObjectEvent::class, new DataObjectEventListener( + DataObjectEventListener::create( function (DataObjectEvent $event) { static::$events[] = $event; }, [SimpleDataObject::class, VersionedDataObject::class] - )); + )->selfRegister(); } protected function setUp(): void From 397b00dae35c0cae102fb423b07953d3939ffca1 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 11:02:43 +1300 Subject: [PATCH 10/26] Fix linting issue --- src/DataObjectEventListener.php | 2 +- src/Event/DataObjectEvent.php | 1 - tests/php/Extension/EventDispatchExtensionTest.php | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/DataObjectEventListener.php b/src/DataObjectEventListener.php index b15a1f0..4ac4254 100644 --- a/src/DataObjectEventListener.php +++ b/src/DataObjectEventListener.php @@ -21,7 +21,7 @@ class DataObjectEventListener { use Injectable; - + /** * Creates a new DataObject event listener. * diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index 29d562d..b0878d0 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -2,7 +2,6 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Event; -use SilverStripe\Core\Extension; use SilverStripe\Core\Injector\Injectable; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; diff --git a/tests/php/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php index 13cc8f2..7e338ee 100644 --- a/tests/php/Extension/EventDispatchExtensionTest.php +++ b/tests/php/Extension/EventDispatchExtensionTest.php @@ -5,11 +5,9 @@ use ArchiPro\Silverstripe\EventDispatcher\DataObjectEventListener; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectEvent; use ArchiPro\Silverstripe\EventDispatcher\Event\Operation; -use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; use ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\SimpleDataObject; use ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\VersionedDataObject; use Revolt\EventLoop; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\Security\Member; use SilverStripe\Security\Security; From 0b8ed50a6043acf3b80773415f80a16b5327f8ba Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 11:24:46 +1300 Subject: [PATCH 11/26] Add DataObjectListener test --- .../DataObjectEventListener.php | 2 +- .../Extension/EventDispatchExtensionTest.php | 2 +- .../Listener/DataObjectEventListenerTest.php | 148 ++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) rename src/{ => Listener}/DataObjectEventListener.php (98%) create mode 100644 tests/php/Listener/DataObjectEventListenerTest.php diff --git a/src/DataObjectEventListener.php b/src/Listener/DataObjectEventListener.php similarity index 98% rename from src/DataObjectEventListener.php rename to src/Listener/DataObjectEventListener.php index 4ac4254..e377647 100644 --- a/src/DataObjectEventListener.php +++ b/src/Listener/DataObjectEventListener.php @@ -1,6 +1,6 @@ receivedEvents = []; + } + + public function testListenerFiltersByClass(): void + { + // Create listener that only handles SimpleDataObject events + $listener = DataObjectEventListener::create( + function (DataObjectEvent $event) { + $this->receivedEvents[] = $event; + }, + [SimpleDataObject::class] + ); + + // Should handle SimpleDataObject event + $simpleEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); + $listener($simpleEvent); + $this->assertCount(1, $this->receivedEvents, 'Listener should handle SimpleDataObject events'); + + // Should not handle VersionedDataObject event + $versionedEvent = DataObjectEvent::create(1, VersionedDataObject::class, Operation::CREATE); + $listener($versionedEvent); + $this->assertCount(1, $this->receivedEvents, 'Listener should not handle VersionedDataObject events'); + } + + public function testListenerHandlesInheritedClasses(): void + { + // Create listener that handles all DataObject events + $listener = DataObjectEventListener::create( + function (DataObjectEvent $event) { + $this->receivedEvents[] = $event; + }, + [DataObject::class] + ); + + // Should handle both SimpleDataObject and VersionedDataObject events + $simpleEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); + $versionedEvent = DataObjectEvent::create(1, VersionedDataObject::class, Operation::CREATE); + + $listener($simpleEvent); + $listener($versionedEvent); + + $this->assertCount(2, $this->receivedEvents, 'Listener should handle events from DataObject subclasses'); + } + + public function testListenerFiltersByOperation(): void + { + // Create listener that only handles CREATE and UPDATE operations + $listener = DataObjectEventListener::create( + function (DataObjectEvent $event) { + $this->receivedEvents[] = $event; + }, + [SimpleDataObject::class], + [Operation::CREATE, Operation::UPDATE] + ); + + // Should handle CREATE event + $createEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); + $listener($createEvent); + $this->assertCount(1, $this->receivedEvents, 'Listener should handle CREATE events'); + + // Should handle UPDATE event + $updateEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::UPDATE); + $listener($updateEvent); + $this->assertCount(2, $this->receivedEvents, 'Listener should handle UPDATE events'); + + // Should not handle DELETE event + $deleteEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::DELETE); + $listener($deleteEvent); + $this->assertCount(2, $this->receivedEvents, 'Listener should not handle DELETE events'); + } + + public function testListenerHandlesAllOperationsWhenNotSpecified(): void + { + // Create listener without specifying operations + $listener = DataObjectEventListener::create( + function (DataObjectEvent $event) { + $this->receivedEvents[] = $event; + }, + [SimpleDataObject::class] + ); + + // Should handle all operations + foreach (Operation::cases() as $operation) { + $event = DataObjectEvent::create(1, SimpleDataObject::class, $operation); + $listener($event); + } + + $this->assertCount( + count(Operation::cases()), + $this->receivedEvents, + 'Listener should handle all operations when none specified' + ); + } + + public function testSelfRegister(): void + { + // Create a mock event service + /** @var MockObject|ListenerProvider $provider */ + $provider = $this->createMock(ListenerProvider::class); + $provider->expects($this->once()) + ->method('addListener') + ->with( + DataObjectEvent::class, + $this->isInstanceOf(DataObjectEventListener::class) + ); + + // Create listener and register with mock service + $listener = DataObjectEventListener::create( + function (DataObjectEvent $event) { + $this->receivedEvents[] = $event; + }, + [SimpleDataObject::class] + ); + $listener->selfRegister($provider); + } +} From 02693868c1ae2d310d096cb62e1ef4378b8402f4 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 12:28:26 +1300 Subject: [PATCH 12/26] Update readme --- README.md | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da6e031..fb2a850 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,182 @@ -# Silverstripe Events Module +# Silverstripe CMS Event Dispatcher Module -This module provides PSR-14 Event Dispatcher integration for Silverstripe CMS with automatic event dispatching for DataObject CRUD operations and versioning actions. +This module adds the ability to dispatch and listen for events in Silverstripe CMS. It's built around Revolt PHP +and AMPHP. It aims to process events asynchronously. It also provides some abstraction to help managing event around +common DataObject operations. -## Installation \ No newline at end of file +## Installation + +```bash +composer require archipro/silverstripe-revolt-event-dispatcher +``` + +## Running the Event Loop + +Because we are using Revolt PHP, you need to run the event loop to process the events. + +Somewhere in your code you need to start the event loop by running `\Revolt\EventLoop::run()`. This will process all the events up to that point. + +A simple approach is to put it at the end of your `public/index.php` file in a `try-finally` block. You can also add a `fastcgi_finish_request()` call to ensure all output is sent before processing the events. + +```php +try { + $kernel = new CoreKernel(BASE_PATH); + $app = new HTTPApplication($kernel); + $response = $app->handle($request); + $response->output(); +} finally { + // This call will complete the request without closing the PHP worker. A nice side effect of this is that your + // event listeners won't block your request from being sent to the client. So you can use them to run slow + // operations like sending emails or doing API calls without delaying the response. + fastcgi_finish_request(); + + // Now we can process the events in the event loop + \Revolt\EventLoop::run(); +} +``` + +## Features +- Automatic event dispatching for DataObject operations (create, update, delete) +- Support for versioned operations (publish, unpublish, archive, restore) +- Asynchronous event handling using Revolt Event Loop + + +## Basic Usage + +### Firing a Custom Event + +```php +use SilverStripe\Core\Injector\Injector; +use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; + +// Create your event class +class MyCustomEvent +{ + public function __construct( + private readonly string $message + ) {} + + public function getMessage(): string + { + return $this->message; + } +} + +// Dispatch the event +$event = new MyCustomEvent('Hello World'); +$service = Injector::inst()->get(EventService::class); +$service->dispatch($event); +``` + +### Adding a Simple Event Listener + +```php +use SilverStripe\Core\Injector\Injector; +use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; + +// Add a listener +$service = Injector::inst()->get(EventService::class); +$service->addListener(MyCustomEvent::class, function(MyCustomEvent $event) { + echo $event->getMessage(); +}); +``` + +### Configuration-based Listeners + +You can register listeners via YAML configuration: + +```yaml +ArchiPro\Silverstripe\EventDispatcher\Service\EventService: + listeners: + MyCustomEvent: + - ['MyApp\EventListener,handleEvent'] +``` + +## DataObject Event Handling + +This module automatically dispatches events for DataObject operations. You can listen for these events using the +`DataObjectEventListener` class. + +### Firing DataObject Events + +Applying the `EventDispatchExtension` to a DataObject will automatically fire events when changes are made to an +instance of that DataObject. + +```yaml + +## This will fire events for SiteTree instances only +SilverStripe\SiteTree\SiteTree: + extensions: + - ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension + +## This will fire events for all DataObjects +SilverStripe\ORM\DataObject: + extensions: + - ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension +``` + +### Listening for DataObject Events + +```php +use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectEvent; +use ArchiPro\Silverstripe\EventDispatcher\Event\Operation; +use ArchiPro\Silverstripe\EventDispatcher\Listener\DataObjectEventListener; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Security\Member; + +// Create a listener for all Member operations +DataObjectEventListener::create( + function (DataObjectEvent $event) { + echo "Operation {$event->getOperation()->value} performed on Member {$event->getObjectID()}"; + }, + [Member::class] +)->selfRegister(); + +// Listen for specific operations on multiple classes +DataObjectEventListener::create( + function (DataObjectEvent $event) { + // Handle create/update operations + }, + [Member::class, Group::class], + [Operation::CREATE, Operation::UPDATE] +)->selfRegister(); +``` + +### Available Operations + +The following operations are automatically tracked: + +- `Operation::CREATE` - When a DataObject is first written +- `Operation::UPDATE` - When an existing DataObject is modified +- `Operation::DELETE` - When a DataObject is deleted +- `Operation::PUBLISH` - When a versioned DataObject is published +- `Operation::UNPUBLISH` - When a versioned DataObject is unpublished +- `Operation::ARCHIVE` - When a versioned DataObject is archived +- `Operation::RESTORE` - When a versioned DataObject is restored from archive + +### Accessing Event Data + +The `DataObjectEvent` class provides several methods to access information about the event: + +```php +DataObjectEventListener::create( + function (DataObjectEvent $event) { + $object = $event->getObject(); // Get the affected DataObject + $class = $event->getObjectClass(); // Get the class name + $operation = $event->getOperation(); // Get the operation type + $version = $event->getVersion(); // Get version number (if versioned) + $member = $event->getMember(); // Get the Member who performed the action + $time = $event->getTimestamp(); // Get when the event occurred + }, + [DataObject::class] +)->selfRegister(); +``` + +`DataObjectEvent` is configured to be serializable so it can easily be stored for later use. + +Note that `DataObjectEvent` doesn't store the actual DataObject instance that caused the event to be fired. +`DataObjectEvent::getObject()` will refetch the latest version of the DataObject from the database ... which will +return `null` if the DataObject has been deleted. + +`DataObjectEvent::getObject(true) will attempt to retrieve the exact version of the DataObject that fired the event, +assuming it was versioned. \ No newline at end of file From 0fc305fda0d0358966e0a0ecb20b1fcbfba16ae4 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 12:33:31 +1300 Subject: [PATCH 13/26] Remove repositories composer config --- composer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index be00039..9342c08 100644 --- a/composer.json +++ b/composer.json @@ -43,11 +43,5 @@ "composer/installers": true, "silverstripe/vendor-plugin": true } - }, - "repositories": [ - { - "type": "github", - "url": "git@github.com:archiprocode/revolt-event-dispatcher.git" - } - ] + } } From 9e945f9c4968beef657716f6c4931e1a0a7a06b4 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 12:34:54 +1300 Subject: [PATCH 14/26] Add CI badge to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb2a850..01200b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Silverstripe CMS Event Dispatcher Module +# Silverstripe CMS RevoltEvent Dispatcher Module (experimental) +[![CI](https://github.com/archiprocode/silverstripe-revolt-event-dispatcher/actions/workflows/ci.yml/badge.svg)](https://github.com/archiprocode/silverstripe-revolt-event-dispatcher/actions/workflows/ci.yml) This module adds the ability to dispatch and listen for events in Silverstripe CMS. It's built around Revolt PHP and AMPHP. It aims to process events asynchronously. It also provides some abstraction to help managing event around From ed7ba728539810b93ba8f46c46c059258ba21ff6 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 10:30:21 +1300 Subject: [PATCH 15/26] Try to instanciate Loaders and listeners with Injector --- src/Service/EventService.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Service/EventService.php b/src/Service/EventService.php index b511604..c0dca12 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -7,6 +7,7 @@ use ArchiPro\Silverstripe\EventDispatcher\Contract\ListenerLoaderInterface; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Injector\Injectable; +use SilverStripe\Core\Injector\Injector; /** * Core service class for handling event dispatching in Silverstripe. @@ -53,6 +54,9 @@ private function registerListeners(): void foreach ($listeners as $eventClass => $listeners) { foreach ($listeners as $listener) { + if (is_string($listener)) { + $listener = Injector::inst()->get($listener); + } $this->addListener($eventClass, $listener); } } @@ -64,6 +68,9 @@ private function registerListeners(): void private function loadListeners(): void { foreach ($this->config()->get('loaders') as $loader) { + if (is_string($loader)) { + $loader = Injector::inst()->get($loader); + } $this->addListenerLoader($loader); } } From 54afefd5afadee612171c50f8adf405dead5452e Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 10:31:19 +1300 Subject: [PATCH 16/26] Add more code sample to readme --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 01200b8..b99651a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,12 @@ common DataObject operations. composer require archipro/silverstripe-revolt-event-dispatcher ``` -## Running the Event Loop +## Features +- Automatic event dispatching for DataObject operations (create, update, delete) +- Support for versioned operations (publish, unpublish, archive, restore) +- Asynchronous event handling using Revolt Event Loop + +## Setting up the Event Loop Because we are using Revolt PHP, you need to run the event loop to process the events. @@ -31,16 +36,19 @@ try { // operations like sending emails or doing API calls without delaying the response. fastcgi_finish_request(); + // Many methods in Silverstripe CMS rely on having a current controller with a request. + $controller = new Controller(); + $controller->setRequest($request); + $controller->pushCurrent(); + // Now we can process the events in the event loop \Revolt\EventLoop::run(); } ``` -## Features -- Automatic event dispatching for DataObject operations (create, update, delete) -- Support for versioned operations (publish, unpublish, archive, restore) -- Asynchronous event handling using Revolt Event Loop +### TODO +- Need to find a an elegant way to run the event loop on `sake` commands. This won't hit `public/index.php`. ## Basic Usage @@ -78,7 +86,7 @@ use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; // Add a listener $service = Injector::inst()->get(EventService::class); $service->addListener(MyCustomEvent::class, function(MyCustomEvent $event) { - echo $event->getMessage(); + error_log('MyCustomEventListener::handleEvent was called'); }); ``` @@ -90,7 +98,54 @@ You can register listeners via YAML configuration: ArchiPro\Silverstripe\EventDispatcher\Service\EventService: listeners: MyCustomEvent: - - ['MyApp\EventListener,handleEvent'] + - ['MyApp\EventListener', 'handleEvent'] +``` + +## Registering many listeners at once with loaders + +You can use listeners loaders to register many listeners at once. + +```php +selfRegister($provider); + } + + public static function onMemberCreated(DataObjectEvent $event): void + { + $member = $event->getObject(); + error_log('Member created: ' . $member->ID); + Email::create() + ->setTo($member->Email) + ->setSubject('Welcome to our site') + ->setFrom('no-reply@example.com') + ->setBody('Welcome to our site') + ->send(); + } +} +``` + +Loaders can be registered in your YAML configuration file: +```yaml +ArchiPro\Silverstripe\EventDispatcher\Service\EventService: + loaders: + - MemberListenerLoader ``` ## DataObject Event Handling @@ -180,4 +235,4 @@ Note that `DataObjectEvent` doesn't store the actual DataObject instance that ca return `null` if the DataObject has been deleted. `DataObjectEvent::getObject(true) will attempt to retrieve the exact version of the DataObject that fired the event, -assuming it was versioned. \ No newline at end of file +assuming it was versioned. From 1db248b56bc0fb867f11d999bcb46ea715af2ab8 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 17:06:58 +1300 Subject: [PATCH 17/26] Be more explicit abotut he return type of DataObjectEvent --- src/Event/DataObjectEvent.php | 10 +++++++--- src/Extension/EventDispatchExtension.php | 12 ++++++------ src/Listener/DataObjectEventListener.php | 9 ++++++--- tests/php/Event/DataObjectEventTest.php | 12 ++++++------ .../php/Listener/DataObjectEventListenerTest.php | 16 ++++++++-------- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index b0878d0..4e20bea 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -24,13 +24,15 @@ * Example usage: * ```php * $event = DataObjectEvent::create( - * $dataObject->ID, * get_class($dataObject), + * $dataObject->ID, * Operation::UPDATE, * $dataObject->Version, * Security::getCurrentUser()?->ID * ); * ``` + * + * @template T of DataObject */ class DataObjectEvent { @@ -42,15 +44,15 @@ class DataObjectEvent private readonly int $timestamp; /** + * @param class-string $objectClass The class name of the affected DataObject * @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 int $objectID, private readonly Operation $operation, private readonly ?int $version = null, private readonly ?int $memberID = null @@ -109,6 +111,8 @@ public function getTimestamp(): int /** * Get the DataObject associated with this event * + * @phpstan-return T|null + * * @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 */ diff --git a/src/Extension/EventDispatchExtension.php b/src/Extension/EventDispatchExtension.php index 6c35325..e23b1eb 100644 --- a/src/Extension/EventDispatchExtension.php +++ b/src/Extension/EventDispatchExtension.php @@ -27,8 +27,8 @@ public function onAfterWrite(): void { $owner = $this->getOwner(); $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, // 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, @@ -46,8 +46,8 @@ public function onBeforeDelete(): void { $owner = $this->getOwner(); $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, Operation::DELETE, $owner->hasExtension(Versioned::class) ? $owner->Version : null, Security::getCurrentUser()?->ID @@ -67,8 +67,8 @@ public function onAfterPublish(): void } $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, Operation::PUBLISH, $owner->Version, Security::getCurrentUser()?->ID @@ -88,8 +88,8 @@ public function onAfterUnpublish(): void } $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, Operation::UNPUBLISH, $owner->Version, Security::getCurrentUser()?->ID @@ -109,8 +109,8 @@ public function onAfterArchive(): void } $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, Operation::ARCHIVE, $owner->Version, Security::getCurrentUser()?->ID @@ -130,8 +130,8 @@ public function onAfterRestore(): void } $event = DataObjectEvent::create( - $owner->ID, get_class($owner), + $owner->ID, Operation::RESTORE, $owner->Version, Security::getCurrentUser()?->ID diff --git a/src/Listener/DataObjectEventListener.php b/src/Listener/DataObjectEventListener.php index e377647..9157bc6 100644 --- a/src/Listener/DataObjectEventListener.php +++ b/src/Listener/DataObjectEventListener.php @@ -17,6 +17,8 @@ * This listener can be configured to only handle specific operations (create, update, delete etc) * and specific DataObject classes. When an event matches the configured criteria, the callback * is executed with the event. + * + * @template T of DataObject */ class DataObjectEventListener { @@ -25,8 +27,9 @@ class DataObjectEventListener /** * Creates a new DataObject event listener. * - * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches - * @param class-string[] $classes Array of DataObject class names to listen for + * @template T of DataObject + * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches + * @param class-string[] $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( @@ -56,7 +59,7 @@ public function selfRegister(ListenerProvider|EventService $provider = null): vo * Checks if the event matches the configured operations and classes, * and executes the callback if it does. * - * @param DataObjectEvent $event The event to handle + * @param DataObjectEvent $event The event to handle */ public function __invoke(DataObjectEvent $event): void { diff --git a/tests/php/Event/DataObjectEventTest.php b/tests/php/Event/DataObjectEventTest.php index 31e47ed..7fef75a 100644 --- a/tests/php/Event/DataObjectEventTest.php +++ b/tests/php/Event/DataObjectEventTest.php @@ -20,7 +20,7 @@ class DataObjectEventTest extends SapphireTest public function testEventCreation(): void { - $event = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE, null, 1); + $event = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE, null, 1); $this->assertEquals(1, $event->getObjectID()); $this->assertEquals(SimpleDataObject::class, $event->getObjectClass()); @@ -35,7 +35,7 @@ public function testGetObject(): void /** @var SimpleDataObject $object */ $object = $this->objFromFixture(SimpleDataObject::class, 'object1'); - $event = DataObjectEvent::create($object->ID, SimpleDataObject::class, Operation::UPDATE); + $event = DataObjectEvent::create(SimpleDataObject::class, $object->ID, Operation::UPDATE); $this->assertNotNull($event->getObject()); $this->assertEquals($object->ID, $event->getObject()->ID); @@ -50,7 +50,7 @@ public function testGetVersionedObject(): void $object->Title = 'Updated Title'; $object->write(); - $event = DataObjectEvent::create($object->ID, VersionedDataObject::class, Operation::UPDATE, $object->Version); + $event = DataObjectEvent::create(VersionedDataObject::class, $object->ID, Operation::UPDATE, $object->Version); // Get current version $currentObject = $event->getObject(false); @@ -61,7 +61,7 @@ public function testGetVersionedObject(): void $this->assertEquals('Updated Title', $versionedObject->Title); // Get previous version - $previousEvent = DataObjectEvent::create($object->ID, VersionedDataObject::class, Operation::UPDATE, $object->Version - 1); + $previousEvent = DataObjectEvent::create(VersionedDataObject::class, $object->ID, Operation::UPDATE, $object->Version - 1); $previousVersion = $previousEvent->getObject(true); $this->assertEquals('Original Title', $previousVersion->Title); } @@ -71,7 +71,7 @@ public function testGetMember(): void /** @var Member $member */ $member = $this->objFromFixture(Member::class, 'member1'); - $event = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE, null, $member->ID); + $event = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE, null, $member->ID); $this->assertNotNull($event->getMember()); $this->assertEquals($member->ID, $event->getMember()->ID); @@ -79,7 +79,7 @@ public function testGetMember(): void public function testSerialization(): void { - $event = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE, 2, 3); + $event = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE, 2, 3); $serialized = serialize($event); /** @var DataObjectEvent $unserialized */ diff --git a/tests/php/Listener/DataObjectEventListenerTest.php b/tests/php/Listener/DataObjectEventListenerTest.php index 6563ce0..84431a5 100644 --- a/tests/php/Listener/DataObjectEventListenerTest.php +++ b/tests/php/Listener/DataObjectEventListenerTest.php @@ -44,12 +44,12 @@ function (DataObjectEvent $event) { ); // Should handle SimpleDataObject event - $simpleEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); + $simpleEvent = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE); $listener($simpleEvent); $this->assertCount(1, $this->receivedEvents, 'Listener should handle SimpleDataObject events'); // Should not handle VersionedDataObject event - $versionedEvent = DataObjectEvent::create(1, VersionedDataObject::class, Operation::CREATE); + $versionedEvent = DataObjectEvent::create(VersionedDataObject::class, 1, Operation::CREATE); $listener($versionedEvent); $this->assertCount(1, $this->receivedEvents, 'Listener should not handle VersionedDataObject events'); } @@ -65,8 +65,8 @@ function (DataObjectEvent $event) { ); // Should handle both SimpleDataObject and VersionedDataObject events - $simpleEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); - $versionedEvent = DataObjectEvent::create(1, VersionedDataObject::class, Operation::CREATE); + $simpleEvent = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE); + $versionedEvent = DataObjectEvent::create(VersionedDataObject::class, 1, Operation::CREATE); $listener($simpleEvent); $listener($versionedEvent); @@ -86,17 +86,17 @@ function (DataObjectEvent $event) { ); // Should handle CREATE event - $createEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::CREATE); + $createEvent = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE); $listener($createEvent); $this->assertCount(1, $this->receivedEvents, 'Listener should handle CREATE events'); // Should handle UPDATE event - $updateEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::UPDATE); + $updateEvent = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::UPDATE); $listener($updateEvent); $this->assertCount(2, $this->receivedEvents, 'Listener should handle UPDATE events'); // Should not handle DELETE event - $deleteEvent = DataObjectEvent::create(1, SimpleDataObject::class, Operation::DELETE); + $deleteEvent = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::DELETE); $listener($deleteEvent); $this->assertCount(2, $this->receivedEvents, 'Listener should not handle DELETE events'); } @@ -113,7 +113,7 @@ function (DataObjectEvent $event) { // Should handle all operations foreach (Operation::cases() as $operation) { - $event = DataObjectEvent::create(1, SimpleDataObject::class, $operation); + $event = DataObjectEvent::create(SimpleDataObject::class, 1, $operation); $listener($event); } From e857cc7de7131c4a433f0056c990adbc1fb172bd Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 17:10:52 +1300 Subject: [PATCH 18/26] Fix linting issue --- src/Event/DataObjectEvent.php | 8 ++++---- src/Listener/DataObjectEventListener.php | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index 4e20bea..04c1722 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -45,10 +45,10 @@ class DataObjectEvent /** * @param class-string $objectClass The class name of the affected DataObject - * @param int $objectID The ID 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 + * @param int $objectID The ID 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 string $objectClass, diff --git a/src/Listener/DataObjectEventListener.php b/src/Listener/DataObjectEventListener.php index 9157bc6..bb7f569 100644 --- a/src/Listener/DataObjectEventListener.php +++ b/src/Listener/DataObjectEventListener.php @@ -28,9 +28,10 @@ class DataObjectEventListener * Creates a new DataObject event listener. * * @template T of DataObject + * * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches - * @param class-string[] $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. + * @param class-string[] $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, From 8618512cd7a7095de6b253c5c4d81fb3f44a86bf Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 12 Nov 2024 10:38:21 +1300 Subject: [PATCH 19/26] Add option to suppress Event dispatch for testing --- src/Service/EventService.php | 22 +++++++++++++++++ tests/php/Service/EventServiceTest.php | 33 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/Service/EventService.php b/src/Service/EventService.php index c0dca12..81c718a 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -34,6 +34,9 @@ class EventService */ private static array $loaders = []; + /** Whether events should be suppressed from being dispatched. Used for testing. */ + private bool $suppressDispatch = false; + public function __construct( private readonly AsyncEventDispatcher $dispatcher, private readonly ListenerProvider $listenerProvider @@ -104,6 +107,9 @@ public function addListenerLoader(ListenerLoaderInterface $loader): void */ public function dispatch(object $event): object { + if ($this->suppressDispatch) { + return $event; + } return $this->dispatcher->dispatch($event); } @@ -114,4 +120,20 @@ public function getListenerProvider(): ListenerProvider { return $this->listenerProvider; } + + /** + * Enables event dispatching. Use when testing to avoid side effects. + */ + public function enableDispatch(): void + { + $this->suppressDispatch = false; + } + + /** + * Disables event dispatching. Use when testing to avoid side effects. + */ + public function disableDispatch(): void + { + $this->suppressDispatch = true; + } } diff --git a/tests/php/Service/EventServiceTest.php b/tests/php/Service/EventServiceTest.php index 4fe9416..3a04e9d 100644 --- a/tests/php/Service/EventServiceTest.php +++ b/tests/php/Service/EventServiceTest.php @@ -92,4 +92,37 @@ public function testEventDispatchWithConfiguredLoader(): void // Assert loader was used and listener was called $this->assertTrue($loader->eventFired, 'Configured event listener should have been called'); } + + public function testEventDispatchWithDisabledDispatch(): void + { + // Create test event + $event = new class () { + public bool $handled = false; + }; + + // Create event service with real implementations + $service = Injector::inst()->get(EventService::class); + // Add test listener + $service->addListener(get_class($event), function ($event) { + $event->handled = true; + }); + + // Dispatch event + $service->disableDispatch(); + $result = $service->dispatch($event); + + EventLoop::run(); + + // Assert listener was called + $this->assertFalse($result->handled, 'Event listener should not have been called when dispatch is disabled'); + + // Re-enabled dispatch + $service->enableDispatch(); + $result = $service->dispatch($event); + + EventLoop::run(); + + // Assert listener was called + $this->assertTrue($result->handled, 'Event listener should have been called when dispatch is re-enabled'); + } } From e4105337ddcfdd88269fca34dbbfd4eb75a9cc9a Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 12 Nov 2024 14:06:16 +1300 Subject: [PATCH 20/26] Add more detail about testing to README --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index b99651a..eafae9f 100644 --- a/README.md +++ b/README.md @@ -236,3 +236,68 @@ return `null` if the DataObject has been deleted. `DataObjectEvent::getObject(true) will attempt to retrieve the exact version of the DataObject that fired the event, assuming it was versioned. + +## Testing Your Events + +### Writing Event Tests + +When testing your event listeners, you'll need to: +1. Dispatch your events +2. Run the event loop +3. Assert the expected outcomes + +Here's an example test: + +```php +use Revolt\EventLoop; +use SilverStripe\Dev\SapphireTest; +use SilverStripe\Core\Injector\Injector; +use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; + +class MyEventTest extends SapphireTest +{ + public function testMyCustomEvent(): void + { + // Create your test event + $event = new MyCustomEvent('test message'); + + // Get the event service + $service = Injector::inst()->get(EventService::class); + + // Add your test listener ... or if you have already + $wasCalled = false; + $service->addListener( + MyCustomEvent::class, + [MyCustomEventListener::class, 'handleEvent'] + ); + + // Dispatch the event + $service->dispatch($event); + + // Run the event loop to process events + EventLoop::run(); + + // Assert your listener was called + $this->assertTrue( + MyCustomEventListener::wasCalled(), + 'Assert some side effect of the event being handled' + ); + } +} +``` + +### Disabling event dispatching + +You can disable event dispatching for test to avoid side affects from irrelevant events that might be fired while +scaffolding fixtures. + +Call `EventService::singleton()->disableDispatch()` to disable event dispatching while setting up your test. + +When you are ready to start running your test, call `EventService::singleton()->enableDispatch()` to start listening for +events again. + +### Important Testing Notes + +- Always remember to run `EventLoop::run()` after dispatching events +- Events are processed asynchronously, so assertions should happen after running the event loop +- For DataObject events, make sure your test class has the `EventDispatchExtension` applied From aa2593f0f58d4157e240c2d8696b3797b9f03467 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 17:10:28 +1300 Subject: [PATCH 21/26] Enable PHPStan CI --- .github/workflows/ci.yml | 5 +++- .gitignore | 3 ++- phpstan.neon | 12 ++++----- src/Event/DataObjectEvent.php | 4 ++- src/Extension/EventDispatchExtension.php | 34 +++++++++++++++++------- src/Listener/DataObjectEventListener.php | 13 ++++----- src/Service/EventService.php | 5 +++- tests/php/Event/DataObjectEventTest.php | 9 ++++++- 8 files changed, 57 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb42d42..ff377ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,4 +43,7 @@ jobs: run: composer install --prefer-dist --no-progress - name: Check coding standards - run: php-cs-fixer fix --dry-run --diff \ No newline at end of file + run: php-cs-fixer fix --dry-run --diff + + - name: Static Analysis + run: vendor/bin/phpstan analyse \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a59061..3d2bf30 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ .DS_Store composer.lock /public/ -.php-cs-fixer.cache \ No newline at end of file +.php-cs-fixer.cache +phpstan.cache/ \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index 652fa71..a223250 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,12 +3,10 @@ parameters: paths: - src - tests + tmpDir: phpstan.cache excludePaths: - - vendor/* - silverstripe: - checkUnusedViewVariables: false + analyseAndScan: + - vendor ignoreErrors: - - '#Access to an undefined property .+::\$owner#' - - '#Call to an undefined method .+::hasExtension\(\)#' -includes: - - vendor/symbiote/silverstripe-phpstan/phpstan.neon \ No newline at end of file + - '#Static property ArchiPro\\Silverstripe\\EventDispatcher\\Service\\EventService::\$listeners is never read, only written\.#' + - '#Static property ArchiPro\\Silverstripe\\EventDispatcher\\Service\\EventService::\$loaders is never read, only written\.#' diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index 04c1722..40d7f2c 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -123,7 +123,9 @@ public function getObject(bool $useVersion = false): ?DataObject } if (!$useVersion || empty($this->version)) { - return DataObject::get_by_id($this->objectClass, $this->objectID, false); + /** @var T|null $object*/ + $object = DataObject::get($this->objectClass)->byID($this->objectID); + return $object; } return Versioned::get_version($this->objectClass, $this->objectID, $this->version); diff --git a/src/Extension/EventDispatchExtension.php b/src/Extension/EventDispatchExtension.php index e23b1eb..a9bafe0 100644 --- a/src/Extension/EventDispatchExtension.php +++ b/src/Extension/EventDispatchExtension.php @@ -2,6 +2,7 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Extension; +use Amp\Future; use ArchiPro\Silverstripe\EventDispatcher\Event\DataObjectEvent; use ArchiPro\Silverstripe\EventDispatcher\Event\Operation; use ArchiPro\Silverstripe\EventDispatcher\Service\EventService; @@ -14,9 +15,8 @@ /** * Extension that adds event dispatching capabilities to DataObjects. * - * @property DataObject|Versioned $owner - * - * @method DataObject getOwner() + * @phpstan-template T of DataObject + * @phpstan-extends Extension */ class EventDispatchExtension extends Extension { @@ -32,7 +32,7 @@ public function onAfterWrite(): void // 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, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -49,7 +49,7 @@ public function onBeforeDelete(): void get_class($owner), $owner->ID, Operation::DELETE, - $owner->hasExtension(Versioned::class) ? $owner->Version : null, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -70,7 +70,7 @@ public function onAfterPublish(): void get_class($owner), $owner->ID, Operation::PUBLISH, - $owner->Version, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -91,7 +91,7 @@ public function onAfterUnpublish(): void get_class($owner), $owner->ID, Operation::UNPUBLISH, - $owner->Version, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -112,7 +112,7 @@ public function onAfterArchive(): void get_class($owner), $owner->ID, Operation::ARCHIVE, - $owner->Version, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -133,7 +133,7 @@ public function onAfterRestore(): void get_class($owner), $owner->ID, Operation::RESTORE, - $owner->Version, + $this->getVersion(), Security::getCurrentUser()?->ID ); @@ -142,9 +142,23 @@ public function onAfterRestore(): void /** * Dispatches an event using the EventService + * + * @phpstan-param DataObjectEvent $event + * @phpstan-return Future> */ - protected function dispatchEvent(DataObjectEvent $event): DataObjectEvent + protected function dispatchEvent(DataObjectEvent $event): Future { return Injector::inst()->get(EventService::class)->dispatch($event); } + + private function getVersion(): ?int + { + $owner = $this->getOwner(); + if (!$owner->hasExtension(Versioned::class)) { + return null; + } + + /** @var Versioned $owner */ + return $owner->Version; + } } diff --git a/src/Listener/DataObjectEventListener.php b/src/Listener/DataObjectEventListener.php index bb7f569..a520547 100644 --- a/src/Listener/DataObjectEventListener.php +++ b/src/Listener/DataObjectEventListener.php @@ -24,19 +24,20 @@ class DataObjectEventListener { use Injectable; + /** @var Operation[] */ + private array $operations; + /** * Creates a new DataObject event listener. * - * @template T of DataObject - * * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches * @param class-string[] $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. + * @param Operation[] $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 + private readonly Closure $callback, + private readonly array $classes, + array $operations = null ) { $this->operations = $operations ?? Operation::cases(); } diff --git a/src/Service/EventService.php b/src/Service/EventService.php index 81c718a..d30b56b 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -22,7 +22,6 @@ class EventService /** * @config - * * @var array> Map of event class names to arrays of listener callbacks */ private static array $listeners = []; @@ -80,6 +79,10 @@ private function loadListeners(): void /** * Adds a listener to the event service + * + * @template T of object + * @param class-string $event The event class name + * @param callable(T): void $listener The listener callback */ public function addListener(string $event, callable $listener): void { diff --git a/tests/php/Event/DataObjectEventTest.php b/tests/php/Event/DataObjectEventTest.php index 7fef75a..6c159b1 100644 --- a/tests/php/Event/DataObjectEventTest.php +++ b/tests/php/Event/DataObjectEventTest.php @@ -11,8 +11,10 @@ class DataObjectEventTest extends SapphireTest { + /** @var string */ protected static $fixture_file = 'DataObjectEventTest.yml'; + /** @var string[] */ protected static $extra_dataobjects = [ SimpleDataObject::class, VersionedDataObject::class, @@ -50,18 +52,23 @@ public function testGetVersionedObject(): void $object->Title = 'Updated Title'; $object->write(); + /** @var DataObjectEvent $event */ $event = DataObjectEvent::create(VersionedDataObject::class, $object->ID, Operation::UPDATE, $object->Version); // Get current version + /** @var VersionedDataObject $currentObject */ $currentObject = $event->getObject(false); $this->assertEquals('Updated Title', $currentObject->Title); // Get specific version + /** @var VersionedDataObject $versionedObject */ $versionedObject = $event->getObject(true); $this->assertEquals('Updated Title', $versionedObject->Title); // Get previous version + /** @var DataObjectEvent $previousEvent */ $previousEvent = DataObjectEvent::create(VersionedDataObject::class, $object->ID, Operation::UPDATE, $object->Version - 1); + /** @var VersionedDataObject $previousVersion */ $previousVersion = $previousEvent->getObject(true); $this->assertEquals('Original Title', $previousVersion->Title); } @@ -82,7 +89,7 @@ public function testSerialization(): void $event = DataObjectEvent::create(SimpleDataObject::class, 1, Operation::CREATE, 2, 3); $serialized = serialize($event); - /** @var DataObjectEvent $unserialized */ + /** @var DataObjectEvent $unserialized */ $unserialized = unserialize($serialized); $this->assertEquals(1, $unserialized->getObjectID()); From de9b9e92be7ece43b0a736b4a23f6e470390a7db Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 20:36:29 +1300 Subject: [PATCH 22/26] Fix all PHPStan warning --- phpstan.neon | 4 ++-- tests/php/Extension/EventDispatchExtensionTest.php | 7 +++++-- tests/php/Listener/DataObjectEventListenerTest.php | 6 ++++-- tests/php/Mock/SimpleDataObject.php | 4 ++++ tests/php/Mock/VersionedDataObject.php | 3 +++ tests/php/TestListenerLoader.php | 5 +++++ 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index a223250..562c415 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,5 +8,5 @@ parameters: analyseAndScan: - vendor ignoreErrors: - - '#Static property ArchiPro\\Silverstripe\\EventDispatcher\\Service\\EventService::\$listeners is never read, only written\.#' - - '#Static property ArchiPro\\Silverstripe\\EventDispatcher\\Service\\EventService::\$loaders is never read, only written\.#' + - '#Static property .* is never read, only written\.#' + - '#Offset \d+ does not exist on array\{\}#' diff --git a/tests/php/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php index b62c081..2d1cbda 100644 --- a/tests/php/Extension/EventDispatchExtensionTest.php +++ b/tests/php/Extension/EventDispatchExtensionTest.php @@ -11,18 +11,21 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Security\Member; use SilverStripe\Security\Security; +use SilverStripe\ORM\DataObject; class EventDispatchExtensionTest extends SapphireTest { + /** @var string */ protected static $fixture_file = 'EventDispatchExtensionTest.yml'; + /** @var class-string[] */ protected static $extra_dataobjects = [ SimpleDataObject::class, VersionedDataObject::class, ]; - /** @var DataObjectEvent[] */ - private static array $events = []; + /** @var DataObjectEvent[] */ + protected static array $events = []; public static function setUpBeforeClass(): void { diff --git a/tests/php/Listener/DataObjectEventListenerTest.php b/tests/php/Listener/DataObjectEventListenerTest.php index 84431a5..9467958 100644 --- a/tests/php/Listener/DataObjectEventListenerTest.php +++ b/tests/php/Listener/DataObjectEventListenerTest.php @@ -20,12 +20,14 @@ */ class DataObjectEventListenerTest extends SapphireTest { + /** @var class-string[] */ protected static $extra_dataobjects = [ SimpleDataObject::class, VersionedDataObject::class, ]; - private array $receivedEvents = []; + /** @var DataObjectEvent[] */ + protected array $receivedEvents = []; protected function setUp(): void { @@ -127,7 +129,7 @@ function (DataObjectEvent $event) { public function testSelfRegister(): void { // Create a mock event service - /** @var MockObject|ListenerProvider $provider */ + /** @var MockObject&ListenerProvider $provider */ $provider = $this->createMock(ListenerProvider::class); $provider->expects($this->once()) ->method('addListener') diff --git a/tests/php/Mock/SimpleDataObject.php b/tests/php/Mock/SimpleDataObject.php index 827d00f..fc79b23 100644 --- a/tests/php/Mock/SimpleDataObject.php +++ b/tests/php/Mock/SimpleDataObject.php @@ -5,18 +5,22 @@ use ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; +use SilverStripe\Core\Extension; /** * @property string $Title */ class SimpleDataObject extends DataObject implements TestOnly { + private static string $table_name = 'EventDispatcher_SimpleDataObject'; + /** @var array */ private static array $db = [ 'Title' => 'Varchar', ]; + /** @var class-string>[] */ private static array $extensions = [ EventDispatchExtension::class, ]; diff --git a/tests/php/Mock/VersionedDataObject.php b/tests/php/Mock/VersionedDataObject.php index dc3030f..e9e830d 100644 --- a/tests/php/Mock/VersionedDataObject.php +++ b/tests/php/Mock/VersionedDataObject.php @@ -5,6 +5,7 @@ use ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; +use SilverStripe\Core\Extension; use SilverStripe\Versioned\Versioned; /** @@ -16,10 +17,12 @@ class VersionedDataObject extends DataObject implements TestOnly { private static string $table_name = 'EventDispatcher_VersionedDataObject'; + /** @var array */ private static array $db = [ 'Title' => 'Varchar', ]; + /** @var class-string>[] */ private static array $extensions = [ EventDispatchExtension::class, Versioned::class, diff --git a/tests/php/TestListenerLoader.php b/tests/php/TestListenerLoader.php index c8e6301..460446c 100644 --- a/tests/php/TestListenerLoader.php +++ b/tests/php/TestListenerLoader.php @@ -8,12 +8,17 @@ /** * This test loader will listen for the event provided in the constructor * and set the eventFired property to true when the event is fired. + * + * @template T of object */ class TestListenerLoader implements ListenerLoaderInterface { public bool $loaded = false; public bool $eventFired = false; + /** + * @param class-string $eventName + */ public function __construct( private string $eventName ) { From 017bd8bb95698239355680ad5d0ed35a01961eeb Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 20:38:54 +1300 Subject: [PATCH 23/26] Fix linting issue --- src/Event/DataObjectEvent.php | 2 +- src/Extension/EventDispatchExtension.php | 2 ++ src/Listener/DataObjectEventListener.php | 2 +- src/Service/EventService.php | 4 +++- tests/php/Extension/EventDispatchExtensionTest.php | 2 +- tests/php/Mock/SimpleDataObject.php | 3 +-- tests/php/Mock/VersionedDataObject.php | 2 +- tests/php/TestListenerLoader.php | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Event/DataObjectEvent.php b/src/Event/DataObjectEvent.php index 40d7f2c..0b6bdc4 100644 --- a/src/Event/DataObjectEvent.php +++ b/src/Event/DataObjectEvent.php @@ -123,7 +123,7 @@ public function getObject(bool $useVersion = false): ?DataObject } if (!$useVersion || empty($this->version)) { - /** @var T|null $object*/ + /** @var T|null $object */ $object = DataObject::get($this->objectClass)->byID($this->objectID); return $object; } diff --git a/src/Extension/EventDispatchExtension.php b/src/Extension/EventDispatchExtension.php index a9bafe0..23481d4 100644 --- a/src/Extension/EventDispatchExtension.php +++ b/src/Extension/EventDispatchExtension.php @@ -16,6 +16,7 @@ * Extension that adds event dispatching capabilities to DataObjects. * * @phpstan-template T of DataObject + * * @phpstan-extends Extension */ class EventDispatchExtension extends Extension @@ -144,6 +145,7 @@ public function onAfterRestore(): void * Dispatches an event using the EventService * * @phpstan-param DataObjectEvent $event + * * @phpstan-return Future> */ protected function dispatchEvent(DataObjectEvent $event): Future diff --git a/src/Listener/DataObjectEventListener.php b/src/Listener/DataObjectEventListener.php index a520547..eb9b6cc 100644 --- a/src/Listener/DataObjectEventListener.php +++ b/src/Listener/DataObjectEventListener.php @@ -32,7 +32,7 @@ class DataObjectEventListener * * @param Closure(DataObjectEvent): void $callback Callback to execute when an event matches * @param class-string[] $classes Array of DataObject class names to listen for - * @param Operation[] $operations Array of operations to listen for. If null, listens for all operations. + * @param Operation[] $operations Array of operations to listen for. If null, listens for all operations. */ public function __construct( private readonly Closure $callback, diff --git a/src/Service/EventService.php b/src/Service/EventService.php index d30b56b..e15572f 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -22,6 +22,7 @@ class EventService /** * @config + * * @var array> Map of event class names to arrays of listener callbacks */ private static array $listeners = []; @@ -81,7 +82,8 @@ private function loadListeners(): void * Adds a listener to the event service * * @template T of object - * @param class-string $event The event class name + * + * @param class-string $event The event class name * @param callable(T): void $listener The listener callback */ public function addListener(string $event, callable $listener): void diff --git a/tests/php/Extension/EventDispatchExtensionTest.php b/tests/php/Extension/EventDispatchExtensionTest.php index 2d1cbda..746350d 100644 --- a/tests/php/Extension/EventDispatchExtensionTest.php +++ b/tests/php/Extension/EventDispatchExtensionTest.php @@ -9,9 +9,9 @@ use ArchiPro\Silverstripe\EventDispatcher\Tests\Mock\VersionedDataObject; use Revolt\EventLoop; use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; use SilverStripe\Security\Security; -use SilverStripe\ORM\DataObject; class EventDispatchExtensionTest extends SapphireTest { diff --git a/tests/php/Mock/SimpleDataObject.php b/tests/php/Mock/SimpleDataObject.php index fc79b23..164b267 100644 --- a/tests/php/Mock/SimpleDataObject.php +++ b/tests/php/Mock/SimpleDataObject.php @@ -3,16 +3,15 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Tests\Mock; use ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension; +use SilverStripe\Core\Extension; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; -use SilverStripe\Core\Extension; /** * @property string $Title */ class SimpleDataObject extends DataObject implements TestOnly { - private static string $table_name = 'EventDispatcher_SimpleDataObject'; /** @var array */ diff --git a/tests/php/Mock/VersionedDataObject.php b/tests/php/Mock/VersionedDataObject.php index e9e830d..b9e46b3 100644 --- a/tests/php/Mock/VersionedDataObject.php +++ b/tests/php/Mock/VersionedDataObject.php @@ -3,9 +3,9 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Tests\Mock; use ArchiPro\Silverstripe\EventDispatcher\Extension\EventDispatchExtension; +use SilverStripe\Core\Extension; use SilverStripe\Dev\TestOnly; use SilverStripe\ORM\DataObject; -use SilverStripe\Core\Extension; use SilverStripe\Versioned\Versioned; /** diff --git a/tests/php/TestListenerLoader.php b/tests/php/TestListenerLoader.php index 460446c..6310f8f 100644 --- a/tests/php/TestListenerLoader.php +++ b/tests/php/TestListenerLoader.php @@ -8,7 +8,7 @@ /** * This test loader will listen for the event provided in the constructor * and set the eventFired property to true when the event is fired. - * + * * @template T of object */ class TestListenerLoader implements ListenerLoaderInterface From fedec8b2e8e1276fae07cc2eda2d3a250a79efc6 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 22:05:52 +1300 Subject: [PATCH 24/26] Implement peer review feedback --- README.md | 13 +++++----- src/Service/EventService.php | 9 +++++-- tests/php/Service/EventServiceTest.php | 33 +++++++++----------------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index eafae9f..42061ce 100644 --- a/README.md +++ b/README.md @@ -121,16 +121,15 @@ class MemberListenerLoader implements ListenerLoaderInterface public function loadListeners(ListenerProvider $provider): void { DataObjectEventListener::create( - Closure::fromCallable([self::class, 'onMemberCreated']), + Closure::fromCallable([$this, 'onMemberCreated']), [Member::class], [Operation::CREATE] )->selfRegister($provider); } - public static function onMemberCreated(DataObjectEvent $event): void + public function onMemberCreated(DataObjectEvent $event): void { $member = $event->getObject(); - error_log('Member created: ' . $member->ID); Email::create() ->setTo($member->Email) ->setSubject('Welcome to our site') @@ -298,6 +297,8 @@ events again. ### Important Testing Notes -- Always remember to run `EventLoop::run()` after dispatching events -- Events are processed asynchronously, so assertions should happen after running the event loop -- For DataObject events, make sure your test class has the `EventDispatchExtension` applied +- Events are processed asynchronously by default. You can force processing of events by: + - calling `EventLoop::run()` or + - calling `await()` on the returned value of the `dispatch` method. e.g.: `EventService::dispatch($event)->await()`. +- For DataObject events, make sure your test class applies the `EventDispatchExtension` to the relevant DataObject + classes. diff --git a/src/Service/EventService.php b/src/Service/EventService.php index e15572f..4a35b3a 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -2,6 +2,7 @@ namespace ArchiPro\Silverstripe\EventDispatcher\Service; +use Amp\Future; use ArchiPro\EventDispatcher\AsyncEventDispatcher; use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\Silverstripe\EventDispatcher\Contract\ListenerLoaderInterface; @@ -109,11 +110,15 @@ public function addListenerLoader(ListenerLoaderInterface $loader): void /** * Dispatches an event to all registered listeners + * + * @template T of object + * @param T $event + * @return Future */ - public function dispatch(object $event): object + public function dispatch(object $event): Future { if ($this->suppressDispatch) { - return $event; + return Future::complete($event); } return $this->dispatcher->dispatch($event); } diff --git a/tests/php/Service/EventServiceTest.php b/tests/php/Service/EventServiceTest.php index 3a04e9d..fbc8124 100644 --- a/tests/php/Service/EventServiceTest.php +++ b/tests/php/Service/EventServiceTest.php @@ -10,9 +10,9 @@ class EventServiceTest extends SapphireTest { - protected function setUp(): void + private function getService(): EventService { - parent::setUp(); + return Injector::inst()->get(EventService::class); } public function testEventDispatch(): void @@ -22,8 +22,7 @@ public function testEventDispatch(): void public bool $handled = false; }; - // Create event service with real implementations - $service = Injector::inst()->get(EventService::class); + $service = $this->getService(); // Add test listener $service->addListener(get_class($event), function ($event) { @@ -31,9 +30,7 @@ public function testEventDispatch(): void }); // Dispatch event - $result = $service->dispatch($event); - - EventLoop::run(); + $result = $service->dispatch($event)->await(); // Assert listener was called $this->assertTrue($result->handled, 'Event listener should have been called'); @@ -55,13 +52,10 @@ function ($event) { ], ]); - // Get fresh service instance with config applied - $service = Injector::inst()->get(EventService::class); + $service = $this->getService(); // Dispatch event - $result = $service->dispatch($event); - - EventLoop::run(); + $result = $service->dispatch($event)->await(); // Assert listener was called $this->assertTrue($result->handled, 'Configured event listener should have been called'); @@ -80,8 +74,7 @@ public function testEventDispatchWithConfiguredLoader(): void // Configure loader via config EventService::config()->set('loaders', [$loader]); - // Get fresh service instance with config applied - $service = Injector::inst()->get(EventService::class); + $service = $this->getService(); $this->assertTrue($loader->loaded, 'Loader should have been used'); // Dispatch event @@ -100,8 +93,8 @@ public function testEventDispatchWithDisabledDispatch(): void public bool $handled = false; }; - // Create event service with real implementations - $service = Injector::inst()->get(EventService::class); + $service = $this->getService(); + // Add test listener $service->addListener(get_class($event), function ($event) { $event->handled = true; @@ -109,18 +102,14 @@ public function testEventDispatchWithDisabledDispatch(): void // Dispatch event $service->disableDispatch(); - $result = $service->dispatch($event); - - EventLoop::run(); + $result = $service->dispatch($event)->await(); // Assert listener was called $this->assertFalse($result->handled, 'Event listener should not have been called when dispatch is disabled'); // Re-enabled dispatch $service->enableDispatch(); - $result = $service->dispatch($event); - - EventLoop::run(); + $result = $service->dispatch($event)->await(); // Assert listener was called $this->assertTrue($result->handled, 'Event listener should have been called when dispatch is re-enabled'); From 5b7ead7634dc3891914b0f81c6710460964c33fd Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 22:12:20 +1300 Subject: [PATCH 25/26] Fix some linting issue --- src/Service/EventService.php | 2 ++ tests/php/Service/EventServiceTest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Service/EventService.php b/src/Service/EventService.php index 4a35b3a..cf64f92 100644 --- a/src/Service/EventService.php +++ b/src/Service/EventService.php @@ -112,7 +112,9 @@ public function addListenerLoader(ListenerLoaderInterface $loader): void * Dispatches an event to all registered listeners * * @template T of object + * * @param T $event + * * @return Future */ public function dispatch(object $event): Future diff --git a/tests/php/Service/EventServiceTest.php b/tests/php/Service/EventServiceTest.php index fbc8124..6e253fd 100644 --- a/tests/php/Service/EventServiceTest.php +++ b/tests/php/Service/EventServiceTest.php @@ -10,7 +10,7 @@ class EventServiceTest extends SapphireTest { - private function getService(): EventService + private function getService(): EventService { return Injector::inst()->get(EventService::class); } From da0f6d46077363acc10dc826b7a8c42b5472ecd6 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 18 Nov 2024 09:44:23 +1300 Subject: [PATCH 26/26] Require version 0.0.0 of archipro/revolt-event-dispatcher --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9342c08..1a898be 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "silverstripe/versioned": "^1.13 || ^2.0", "psr/event-dispatcher": "^1.0", "psr/event-dispatcher-implementation": "^1.0", - "archipro/revolt-event-dispatcher": "dev-master" + "archipro/revolt-event-dispatcher": "^0.0.0" }, "require-dev": { "phpunit/phpunit": "^9.5",