From e875fcf29a5bb3d46d7e0395274fa4a0ac9072a4 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 11 Sep 2023 19:39:35 -0500 Subject: [PATCH 01/77] Require PHP 8.1. --- composer.json | 2 +- docker-compose.yml | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 8cc3610..808d18b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } ], "require": { - "php": "~7.4 || ~8.0", + "php": "~8.1", "crell/ordered-collection": "^1.0", "fig/event-dispatcher-util": "^1.3", "psr/container": "^1.0 || ^2.0", diff --git a/docker-compose.yml b/docker-compose.yml index 6c50fa6..196f4fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,23 +17,3 @@ services: environment: XDEBUG_MODE: "develop,debug" XDEBUG_CONFIG: "client_host=${HOST_IP} idekey=${IDE_KEY} client_port=${XDEBUG_PORT} discover_client_host=1 start_with_request=1" - php80: - build: ./docker/php/80 - volumes: - - ~/.composer:/.composer #uncomment this line to allow usage of local composer cache - - .:/usr/src/myapp - - ./docker/php/80/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - - ./docker/php/conf.d/error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini - environment: - XDEBUG_MODE: "develop,debug" - XDEBUG_CONFIG: "client_host=${HOST_IP} idekey=${IDE_KEY} client_port=${XDEBUG_PORT} discover_client_host=1 start_with_request=1" - php74: - build: ./docker/php/74 - volumes: - - ~/.composer:/.composer #uncomment this line to allow usage of local composer cache - - .:/usr/src/myapp - - ./docker/php/74/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - - ./docker/php/conf.d/error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini - environment: - XDEBUG_MODE: "develop,debug" - XDEBUG_CONFIG: "client_host=${HOST_IP} idekey=${IDE_KEY} client_port=${XDEBUG_PORT} discover_client_host=1 start_with_request=1" From 110eccd0b45f427825e1909e9e8164ffde5a02a7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 11 Sep 2023 20:07:22 -0500 Subject: [PATCH 02/77] Remove version-conditional behavior. --- src/CompiledListenerProviderBase.php | 5 ++--- src/OrderedListenerProvider.php | 12 +++--------- src/ProviderUtilities.php | 5 ----- tests/CompiledListenerProviderAttributeTest.php | 3 --- .../OrderedListenerProviderAttributeServiceTest.php | 3 --- tests/OrderedListenerProviderAttributeTest.php | 3 --- 6 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index 114eb1e..ad60610 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -31,9 +31,8 @@ public function __construct(ContainerInterface $container) */ public function getListenersForEvent(object $event): iterable { - // @todo Switch to ::class syntax in PHP 8. - if (isset($this->optimized[get_class($event)])) { - return $this->optimized[get_class($event)]; + if (isset($this->optimized[$event::class])) { + return $this->optimized[$event::class]; } $count = count($this->listeners); diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 39b2ee4..9dc5071 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -169,13 +169,8 @@ protected function addSubscribersByProxy(string $class, string $service): Listen */ protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array { - // This extra dance needed to keep the code working on PHP < 8.0. It can be removed once - // 8.0 is made a requirement. - $attributes = []; - if (class_exists('ReflectionAttribute', false)) { - $attributes = array_map(static fn (\ReflectionAttribute $attrib): object - => $attrib->newInstance(), $rMethod->getAttributes(ListenerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF)); - } + $attributes = array_map(static fn (\ReflectionAttribute $attrib): object + => $attrib->newInstance(), $rMethod->getAttributes(ListenerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF)); return $attributes; } @@ -192,10 +187,9 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class foreach ($attributes as $attrib) { $params = $rMethod->getParameters(); $paramType = $params[0]->getType(); - // This can simplify to ?-> once we require PHP 8.0. // getName() is not part of the declared reflection API, but it's there. // @phpstan-ignore-next-line - $type = $attrib->type ?? ($paramType ? $paramType->getName() : null); + $type = $attrib->type ?? $paramType?->getName(); if (is_null($type)) { throw InvalidTypeException::fromClassCallable($class, $methodName); } diff --git a/src/ProviderUtilities.php b/src/ProviderUtilities.php index 00e97bc..63d3847 100644 --- a/src/ProviderUtilities.php +++ b/src/ProviderUtilities.php @@ -20,11 +20,6 @@ trait ProviderUtilities */ protected function getAttributes(callable $listener): array { - // Bail out < PHP 8.0. - if (!class_exists('ReflectionAttribute', false)) { - return []; - } - $ref = null; if ($this->isFunctionCallable($listener)) { diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 19357a6..0e92f3e 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -44,9 +44,6 @@ public static function listen(CollectingEvent $event): void } } -/** - * @requires PHP >= 8.0 - */ class CompiledEventDispatcherAttributeTest extends TestCase { use MakeCompiledProviderTrait; diff --git a/tests/OrderedListenerProviderAttributeServiceTest.php b/tests/OrderedListenerProviderAttributeServiceTest.php index 77fc311..9b7c034 100644 --- a/tests/OrderedListenerProviderAttributeServiceTest.php +++ b/tests/OrderedListenerProviderAttributeServiceTest.php @@ -7,9 +7,6 @@ use PHPUnit\Framework\TestCase; -/** - * @requires PHP >= 8.0 - */ class OrderedListenerProviderAttributeServiceTest extends TestCase { public function test_add_subscriber() : void diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index 10d9f97..f8cf732 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -73,9 +73,6 @@ function at_multi_one(CollectingEvent $event): void $event->add('A'); } -/** - * @requires PHP >= 8.0 - */ class OrderedListenerProviderAttributeTest extends TestCase { public function test_id_from_attribute_is_found() : void From b1899536602fdc5fce2b65fd94a6d84199a7525b Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:04:11 -0500 Subject: [PATCH 03/77] Fold all the attributes into one, and only allow one of them to make the code simpler. --- src/Listener.php | 19 +++++++- src/ListenerAfter.php | 10 ++--- src/ListenerBefore.php | 10 ++--- src/ListenerPriority.php | 10 ++--- src/OrderedListenerProvider.php | 45 ++++++++++--------- .../OrderedListenerProviderAttributeTest.php | 1 + 6 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index 4aede2f..a0f4697 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -5,12 +5,27 @@ namespace Crell\Tukio; use Attribute; +use PHPUnit\Util\Exception; -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +/** + * The main attribute to customize a listener. + * + * This attribute handles both priority sorting and topological (before/after) + * sorting. For that reason, it MUST always be used with named arguments. + * Specifying more than one of $priority, $before, or $after is an error. + */ +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class Listener implements ListenerAttribute { public function __construct( public ?string $id = null, + public ?int $priority = null, + public ?string $before = null, + public ?string $after = null, public ?string $type = null, - ) {} + ) { + if (count(\array_filter([$before !== null, $after !== null, $priority !== null])) > 1) { + throw new Exception('TODO: Make this a custom exception'); + } + } } diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index 0fec33c..619d647 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -7,11 +7,9 @@ use Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerAfter implements ListenerAttribute +class ListenerAfter extends Listener { - public function __construct( - public string $after, - public ?string $id = null, - public ?string $type = null, - ) {} + public function __construct(string $after, ?string $id = null, ?string $type = null) { + parent::__construct(id: $id, after: $after, type: $type); + } } diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index ec0525f..8ff50f9 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -7,11 +7,9 @@ use Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerBefore implements ListenerAttribute +class ListenerBefore extends Listener { - public function __construct( - public string $before, - public ?string $id = null, - public ?string $type = null, - ) {} + public function __construct(string $before, ?string $id = null, ?string $type = null) { + parent::__construct(id: $id, before: $before, type: $type); + } } diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index bba108d..7fb22a9 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -7,11 +7,9 @@ use Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerPriority implements ListenerAttribute +class ListenerPriority extends Listener { - public function __construct( - public ?int $priority, - public ?string $id = null, - public ?string $type = null, - ) {} + public function __construct(?int $priority, ?string $id = null, ?string $type = null) { + parent::__construct(id: $id, priority: $priority, type: $type); + } } diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 9dc5071..ee95f06 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -41,30 +41,33 @@ public function getListenersForEvent(object $event): iterable public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ - foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); - if ($attrib instanceof ListenerBefore) { - $generatedId = $this->listeners->addItemBefore($attrib->before, new ListenerEntry($listener, $type), $id); - } elseif ($attrib instanceof ListenerAfter) { - $generatedId = $this->listeners->addItemAfter($attrib->after, new ListenerEntry($listener, $type), $id); - } elseif ($attrib instanceof ListenerPriority) { - $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $type), $attrib->priority, $id); - } else { - $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $type), $priority ?? 0, $id); - } - } - // Return the last id only, because that's all we can do. - return $generatedId; + $attributes = $this->getAttributes($listener); + $def = $attributes[0] ?? new Listener(); + + if ($priority) { + $def->priority = $priority; + } + if ($id) { + $def->id = $id; + } + if ($type) { + $def->type = $type; } - $type = $type ?? $this->getType($listener); - $id = $id ?? $this->getListenerId($listener); + $def->id ??= $this->getListenerId($listener); + $def->type ??= $this->getType($listener); + + if ($def->before) { + $generatedId = $this->listeners->addItemBefore($def->before, new ListenerEntry($listener, $def->type), $def->id); + } elseif ($def->after) { + $generatedId = $this->listeners->addItemAfter($def->after, new ListenerEntry($listener, $def->type), $def->id); + } elseif ($def->priority) { + $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $def->type), $def->priority, $def->id); + } else { + $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $def->type), $priority ?? 0, $def->id); + } - return $this->listeners->addItem(new ListenerEntry($listener, $type), $priority ?? 0, $id); + return $generatedId; } public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index f8cf732..b695121 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -194,6 +194,7 @@ public function test_before_after_methods_win_over_attributes(): void public function test_multiple_attributes_read_separately(): void { + $this->markTestSkipped('We are probably removing this functionality.'); $p = new OrderedListenerProvider(); // Just to make the following lines shorter and easier to read. From a272c67bb6b6c348477cc71e410eb24af1806257 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:07:10 -0500 Subject: [PATCH 04/77] More PHP 8 syntax. --- src/Entry/ListenerEntry.php | 5 +---- src/OrderedListenerProvider.php | 17 +++++++---------- src/ProviderBuilder.php | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Entry/ListenerEntry.php b/src/Entry/ListenerEntry.php index 52f961c..c69b6dc 100644 --- a/src/Entry/ListenerEntry.php +++ b/src/Entry/ListenerEntry.php @@ -14,11 +14,8 @@ class ListenerEntry /** @var callable */ public $listener; - public string $type; - - public function __construct(callable $listener, string $type) + public function __construct(callable $listener, public string $type) { $this->listener = $listener; - $this->type = $type; } } diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index ee95f06..410688f 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -57,15 +57,12 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ $def->id ??= $this->getListenerId($listener); $def->type ??= $this->getType($listener); - if ($def->before) { - $generatedId = $this->listeners->addItemBefore($def->before, new ListenerEntry($listener, $def->type), $def->id); - } elseif ($def->after) { - $generatedId = $this->listeners->addItemAfter($def->after, new ListenerEntry($listener, $def->type), $def->id); - } elseif ($def->priority) { - $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $def->type), $def->priority, $def->id); - } else { - $generatedId = $this->listeners->addItem(new ListenerEntry($listener, $def->type), $priority ?? 0, $def->id); - } + $generatedId = match (true) { + $def->before !== null => $this->listeners->addItemBefore($def->before, new ListenerEntry($listener, $def->type), $def->id), + $def->after !== null => $this->listeners->addItemAfter($def->after, new ListenerEntry($listener, $def->type), $def->id), + $def->priority !== null => $this->listeners->addItem(new ListenerEntry($listener, $def->type), $def->priority, $def->id), + default => $this->listeners->addItem(new ListenerEntry($listener, $def->type), $priority ?? 0, $def->id), + }; return $generatedId; } @@ -206,7 +203,7 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class $this->addListenerService($service, $methodName, $type, null, $attrib->id); } } - } elseif (strpos($methodName, 'on') === 0) { + } elseif (str_starts_with($methodName, 'on')) { $params = $rMethod->getParameters(); $type = $params[0]->getType(); if (is_null($type)) { diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index ccf90ad..7bfd94b 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -148,7 +148,7 @@ public function addSubscriber(string $class, string $service): void $proxy = new ListenerProxy($this, $service, $class); // Explicit registration is opt-in. - if (in_array(SubscriberInterface::class, class_implements($class))) { + if (in_array(SubscriberInterface::class, class_implements($class), true)) { /** @var SubscriberInterface $class */ $class::registerListeners($proxy); } @@ -159,7 +159,7 @@ public function addSubscriber(string $class, string $service): void /** @var \ReflectionMethod $rMethod */ foreach ($methods as $rMethod) { $methodName = $rMethod->getName(); - if (!in_array($methodName, $proxy->getRegisteredMethods()) && strpos($methodName, 'on') === 0) { + if (str_starts_with($methodName, 'on') && !in_array($methodName, $proxy->getRegisteredMethods(), true)) { $params = $rMethod->getParameters(); // getName() is not part of the declared reflection API, but it's there. // @phpstan-ignore-next-line From cb52c63e826bd49f3aefd54d28a1f91452956248 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:14:40 -0500 Subject: [PATCH 05/77] Do a todo. --- src/OrderedListenerProvider.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 410688f..439a9ca 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -135,13 +135,8 @@ public function addSubscriber(string $class, string $service): void try { $methods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); - // Explicitly registered methods ignore all auto-registration mechanisms. - $methods = array_filter($methods, static function(\ReflectionMethod $refm) use ($proxy) { - return !in_array($refm->getName(), $proxy->getRegisteredMethods()); - }); - - // Once we require PHP 7.4, replace the above with this line. - //$methods = array_filter($methods, fn(\ReflectionMethod $r) => !in_array($r->getName(), $proxy->getRegisteredMethods())); + $methods = array_filter($methods, fn(\ReflectionMethod $r) + => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); /** @var \ReflectionMethod $rMethod */ foreach ($methods as $rMethod) { @@ -157,7 +152,7 @@ protected function addSubscribersByProxy(string $class, string $service): Listen $proxy = new ListenerProxy($this, $service, $class); // Explicit registration is opt-in. - if (in_array(SubscriberInterface::class, class_implements($class))) { + if (in_array(SubscriberInterface::class, class_implements($class), true)) { /** @var SubscriberInterface $class */ $class::registerListeners($proxy); } From cada8c81354948b2f568d04a90e238bd09f4c947 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:29:19 -0500 Subject: [PATCH 06/77] Code simplification. --- src/Listener.php | 14 ++++++++++++++ src/OrderedListenerProvider.php | 14 +++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index a0f4697..d0c44fe 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -28,4 +28,18 @@ public function __construct( throw new Exception('TODO: Make this a custom exception'); } } + + /** + * @internal + */ + public function maskWith(?string $id = null, ?int $priority = null, ?string $before = null, ?string $after = null, ?string $type = null): self + { + return new self( + id: $id ?? $this->id, + priority: $priority ?? $this->priority, + before: $before ?? $this->before, + after: $after ?? $this->after, + type: $type ?? $this->type, + ); + } } diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 439a9ca..793586d 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -44,15 +44,7 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ $attributes = $this->getAttributes($listener); $def = $attributes[0] ?? new Listener(); - if ($priority) { - $def->priority = $priority; - } - if ($id) { - $def->id = $id; - } - if ($type) { - $def->type = $type; - } + $def = $def->maskWith(id: $id, priority: $priority, type: $type); $def->id ??= $this->getListenerId($listener); $def->type ??= $this->getType($listener); @@ -71,7 +63,7 @@ public function addListenerBefore(string $before, callable $listener, ?string $i { if ($attributes = $this->getAttributes($listener)) { // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ + /** @var Listener $attrib */ foreach ($attributes as $attrib) { $type = $type ?? $attrib->type ?? $this->getType($listener); $id = $id ?? $attrib->id ?? $this->getListenerId($listener); @@ -92,7 +84,7 @@ public function addListenerAfter(string $after, callable $listener, ?string $id { if ($attributes = $this->getAttributes($listener)) { // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ + /** @var Listener $attrib */ foreach ($attributes as $attrib) { $type = $type ?? $attrib->type ?? $this->getType($listener); $id = $id ?? $attrib->id ?? $this->getListenerId($listener); From a2b6c854582dbe55078a928274518d2f677ee376 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:37:30 -0500 Subject: [PATCH 07/77] More language modernization. --- src/OrderedListenerProvider.php | 30 ++++++++++++++---------------- src/ProviderBuilder.php | 16 ++++++++-------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 793586d..452c931 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -65,8 +65,8 @@ public function addListenerBefore(string $before, callable $listener, ?string $i // @todo We can probably do better than this in the next major. /** @var Listener $attrib */ foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); + $type ??= $attrib->type ?? $this->getType($listener); + $id ??= $attrib->id ?? $this->getListenerId($listener); // The before-ness of this method call always overrides the attribute. $generatedId = $this->listeners->addItemBefore($before, new ListenerEntry($listener, $type), $id); } @@ -74,8 +74,8 @@ public function addListenerBefore(string $before, callable $listener, ?string $i return $generatedId; } - $type = $type ?? $this->getType($listener); - $id = $id ?? $this->getListenerId($listener); + $type ??= $this->getType($listener); + $id ??= $this->getListenerId($listener); return $this->listeners->addItemBefore($before, new ListenerEntry($listener, $type), $id); } @@ -86,8 +86,8 @@ public function addListenerAfter(string $after, callable $listener, ?string $id // @todo We can probably do better than this in the next major. /** @var Listener $attrib */ foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); + $type ??= $attrib->type ?? $this->getType($listener); + $id ??= $attrib->id ?? $this->getListenerId($listener); // The after-ness of this method call always overrides the attribute. $generatedId = $this->listeners->addItemAfter($after, new ListenerEntry($listener, $type), $id); } @@ -95,22 +95,22 @@ public function addListenerAfter(string $after, callable $listener, ?string $id return $generatedId; } - $type = $type ?? $this->getType($listener); - $id = $id ?? $this->getListenerId($listener); + $type ??= $this->getType($listener); + $id ??= $this->getListenerId($listener); return $this->listeners->addItemAfter($after, new ListenerEntry($listener, $type), $id); } public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string { - $id = $id ?? $service . '-' . $method; - $priority = $priority ?? 0; + $id ??= $service . '-' . $method; + $priority ??= 0; return $this->addListener($this->makeListenerForService($service, $method), $priority, $id, $type); } public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string { - $id = $id ?? $service . '-' . $method; + $id ??= $service . '-' . $method; return $this->addListenerBefore($before, $this->makeListenerForService($service, $method), $id, $type); } @@ -170,16 +170,14 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class if (count($attributes)) { // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ + /** @var Listener $attrib */ foreach ($attributes as $attrib) { $params = $rMethod->getParameters(); $paramType = $params[0]->getType(); // getName() is not part of the declared reflection API, but it's there. // @phpstan-ignore-next-line - $type = $attrib->type ?? $paramType?->getName(); - if (is_null($type)) { - throw InvalidTypeException::fromClassCallable($class, $methodName); - } + $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); + if ($attrib instanceof ListenerBefore) { $this->addListenerServiceBefore($attrib->before, $service, $methodName, $type, $attrib->id); } elseif ($attrib instanceof ListenerAfter) { diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 7bfd94b..6050c25 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -71,7 +71,7 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ } $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); - $id = $id ?? $this->getListenerId($listener); + $id ??= $this->getListenerId($listener); return $this->listeners->addItem($entry, $priority ?? 0, $id); } @@ -82,8 +82,8 @@ public function addListenerBefore(string $before, callable $listener, ?string $i // @todo We can probably do better than this in the next major. /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); + $type ??= $attrib->type ?? $this->getType($listener); + $id ??= $attrib->id ?? $this->getListenerId($listener); $entry = $this->getListenerEntry($listener, $type); // The before-ness of this method takes priority over the attribute. $generatedId = $this->listeners->addItemBefore($before, $entry, $id); @@ -92,7 +92,7 @@ public function addListenerBefore(string $before, callable $listener, ?string $i return $generatedId; } - $id = $id ?? $this->getListenerId($listener); + $id ??= $this->getListenerId($listener); $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); return $this->listeners->addItemBefore($before, $entry, $id); } @@ -103,8 +103,8 @@ public function addListenerAfter(string $after, callable $listener, ?string $id // @todo We can probably do better than this in the next major. /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); + $type ??= $attrib->type ?? $this->getType($listener); + $id ??= $attrib->id ?? $this->getListenerId($listener); $entry = $this->getListenerEntry($listener, $type); // The before-ness of this method takes priority over the attribute. $generatedId = $this->listeners->addItemBefore($after, $entry, $id); @@ -114,7 +114,7 @@ public function addListenerAfter(string $after, callable $listener, ?string $id } $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); - $id = $id ?? $this->getListenerId($listener); + $id ??= $this->getListenerId($listener); return $this->listeners->addItemAfter($after, $entry, $id); } @@ -122,7 +122,7 @@ public function addListenerAfter(string $after, callable $listener, ?string $id public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string { $entry = new ListenerServiceEntry($service, $method, $type); - $priority = $priority ?? 0; + $priority ??= 0; return $this->listeners->addItem($entry, $priority, $id); } From 14ad12014adcac31e5a27baabfed281eff32c479 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 13 Sep 2023 20:46:31 -0500 Subject: [PATCH 08/77] Don't bother testing pre-8.1. --- .github/workflows/phpstan.yaml | 2 +- .github/workflows/testing.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml index f58a26b..e4c15ae 100644 --- a/.github/workflows/phpstan.yaml +++ b/.github/workflows/phpstan.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.4', '8.0', '8.1', '8.2' ] + php: [ '8.1', '8.2' ] composer-flags: [ '' ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index cdbfa33..44fe829 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.4', '8.0', '8.1', '8.2' ] + php: [ '8.1', '8.2' ] composer-flags: [ '' ] phpunit-flags: [ '--coverage-text' ] steps: From 350a8584d7dad5e4208e3e3983580543c0dc3c6d Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 11:03:52 -0500 Subject: [PATCH 09/77] PHPStan fixes. --- src/ProviderUtilities.php | 4 ++-- tests/OrderedListenerProviderAttributeTest.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ProviderUtilities.php b/src/ProviderUtilities.php index 63d3847..d114996 100644 --- a/src/ProviderUtilities.php +++ b/src/ProviderUtilities.php @@ -16,7 +16,7 @@ trait ProviderUtilities use ParameterDeriverTrait; /** - * @return array + * @return array */ protected function getAttributes(callable $listener): array { @@ -42,7 +42,7 @@ protected function getAttributes(callable $listener): array return []; } - $attribs = $ref->getAttributes(ListenerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF); + $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF); return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); } diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index b695121..2be9933 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -195,6 +195,7 @@ public function test_before_after_methods_win_over_attributes(): void public function test_multiple_attributes_read_separately(): void { $this->markTestSkipped('We are probably removing this functionality.'); + /* $p = new OrderedListenerProvider(); // Just to make the following lines shorter and easier to read. @@ -209,5 +210,6 @@ public function test_multiple_attributes_read_separately(): void } $this->assertEquals('AAA', implode($event->result())); + */ } } From 96ee5cc52ce1fd86b250199aaa81756d2b97641b Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 11:05:25 -0500 Subject: [PATCH 10/77] PHP 8 syntax. --- src/OrderedListenerProvider.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 452c931..495ca96 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -18,12 +18,9 @@ class OrderedListenerProvider implements ListenerProviderInterface, OrderedProvi */ protected OrderedCollection $listeners; - protected ?ContainerInterface $container; - - public function __construct(?ContainerInterface $container = null) + public function __construct(protected ?ContainerInterface $container = null) { $this->listeners = new OrderedCollection(); - $this->container = $container; } /** From 0f80653f364561614943166fe0e1a4eeb4780873 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:06:48 -0500 Subject: [PATCH 11/77] Convert the internal representation of ordering to a knock-off ADT, which simplifies a lot of code. --- src/Entry/ListenerStaticMethodEntry.php | 1 + src/Listener.php | 25 +---- src/ListenerAfter.php | 2 +- src/ListenerBefore.php | 2 +- src/ListenerPriority.php | 2 +- src/Order.php | 41 ++++++++ src/OrderedListenerProvider.php | 128 +++++++++--------------- src/ProviderBuilder.php | 22 +++- 8 files changed, 118 insertions(+), 105 deletions(-) create mode 100644 src/Order.php diff --git a/src/Entry/ListenerStaticMethodEntry.php b/src/Entry/ListenerStaticMethodEntry.php index 4ed00df..15611b4 100644 --- a/src/Entry/ListenerStaticMethodEntry.php +++ b/src/Entry/ListenerStaticMethodEntry.php @@ -22,6 +22,7 @@ public function __construct(string $class, string $method, string $type) $this->class = $class; $this->method = $method; $this->type = $type; + $this->listener = [$class, $method]; } /** diff --git a/src/Listener.php b/src/Listener.php index d0c44fe..d37f8bd 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -5,7 +5,6 @@ namespace Crell\Tukio; use Attribute; -use PHPUnit\Util\Exception; /** * The main attribute to customize a listener. @@ -19,27 +18,7 @@ class Listener implements ListenerAttribute { public function __construct( public ?string $id = null, - public ?int $priority = null, - public ?string $before = null, - public ?string $after = null, + public ?Order $order = null, public ?string $type = null, - ) { - if (count(\array_filter([$before !== null, $after !== null, $priority !== null])) > 1) { - throw new Exception('TODO: Make this a custom exception'); - } - } - - /** - * @internal - */ - public function maskWith(?string $id = null, ?int $priority = null, ?string $before = null, ?string $after = null, ?string $type = null): self - { - return new self( - id: $id ?? $this->id, - priority: $priority ?? $this->priority, - before: $before ?? $this->before, - after: $after ?? $this->after, - type: $type ?? $this->type, - ); - } + ) {} } diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index 619d647..4fefed4 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -10,6 +10,6 @@ class ListenerAfter extends Listener { public function __construct(string $after, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, after: $after, type: $type); + parent::__construct(id: $id, order: Order::After($after), type: $type); } } diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index 8ff50f9..d5e9e99 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -10,6 +10,6 @@ class ListenerBefore extends Listener { public function __construct(string $before, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, before: $before, type: $type); + parent::__construct(id: $id, order: Order::Before($before), type: $type); } } diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index 7fb22a9..11e5050 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -10,6 +10,6 @@ class ListenerPriority extends Listener { public function __construct(?int $priority, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, priority: $priority, type: $type); + parent::__construct(id: $id, order: Order::Priority($priority), type: $type); } } diff --git a/src/Order.php b/src/Order.php new file mode 100644 index 0000000..1259db1 --- /dev/null +++ b/src/Order.php @@ -0,0 +1,41 @@ +getAttributes($listener); - $def = $attributes[0] ?? new Listener(); - - $def = $def->maskWith(id: $id, priority: $priority, type: $type); - - $def->id ??= $this->getListenerId($listener); - $def->type ??= $this->getType($listener); - - $generatedId = match (true) { - $def->before !== null => $this->listeners->addItemBefore($def->before, new ListenerEntry($listener, $def->type), $def->id), - $def->after !== null => $this->listeners->addItemAfter($def->after, new ListenerEntry($listener, $def->type), $def->id), - $def->priority !== null => $this->listeners->addItem(new ListenerEntry($listener, $def->type), $def->priority, $def->id), - default => $this->listeners->addItem(new ListenerEntry($listener, $def->type), $priority ?? 0, $def->id), + $attrib ??= $this->getAttributes($listener)[0] ?? null; + $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); + $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); + $order ??= $attrib?->order; + + $entry = $this->getListenerEntry($listener, $type); + + return match (true) { + $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), + $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), + $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), + default => $this->listeners->addItem($entry, id: $id), }; - - return $generatedId; } - public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string + protected function getListenerEntry(callable $listener, string $type): ListenerEntry { - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - /** @var Listener $attrib */ - foreach ($attributes as $attrib) { - $type ??= $attrib->type ?? $this->getType($listener); - $id ??= $attrib->id ?? $this->getListenerId($listener); - // The before-ness of this method call always overrides the attribute. - $generatedId = $this->listeners->addItemBefore($before, new ListenerEntry($listener, $type), $id); - } - // Return the last id only, because that's all we can do. - return $generatedId; + // String means it's a function name, and that's safe. + if (is_string($listener)) { + return new ListenerFunctionEntry($listener, $type); + } + // This is how we recognize a static method call. + if (is_array($listener) && isset($listener[0]) && is_string($listener[0])) { + return new ListenerStaticMethodEntry($listener[0], $listener[1], $type); } - $type ??= $this->getType($listener); - $id ??= $this->getListenerId($listener); - - return $this->listeners->addItemBefore($before, new ListenerEntry($listener, $type), $id); + return new ListenerEntry($listener, $type); } - public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string + public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - /** @var Listener $attrib */ - foreach ($attributes as $attrib) { - $type ??= $attrib->type ?? $this->getType($listener); - $id ??= $attrib->id ?? $this->getListenerId($listener); - // The after-ness of this method call always overrides the attribute. - $generatedId = $this->listeners->addItemAfter($after, new ListenerEntry($listener, $type), $id); - } - // Return the last id only, because that's all we can do. - return $generatedId; - } + return $this->listener($listener, ($priority !== null) ? Order::Priority($priority) : null, $id, $type); + } - $type ??= $this->getType($listener); - $id ??= $this->getListenerId($listener); + public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string + { + return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); + } - return $this->listeners->addItemAfter($after, new ListenerEntry($listener, $type), $id); + public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string + { + return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); } public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string @@ -154,7 +140,7 @@ protected function addSubscribersByProxy(string $class, string $service): Listen protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array { $attributes = array_map(static fn (\ReflectionAttribute $attrib): object - => $attrib->newInstance(), $rMethod->getAttributes(ListenerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF)); + => $attrib->newInstance(), $rMethod->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF)); return $attributes; } @@ -162,38 +148,24 @@ protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class, string $service): void { $methodName = $rMethod->getName(); + $params = $rMethod->getParameters(); + + if (count($params) < 1) { + // Skip this method, as it doesn't take arguments. + return; + } $attributes = $this->findAttributesOnMethod($rMethod); + /** @var Listener $attrib */ + $attrib = $attributes[0] ?? null; - if (count($attributes)) { - // @todo We can probably do better than this in the next major. - /** @var Listener $attrib */ - foreach ($attributes as $attrib) { - $params = $rMethod->getParameters(); - $paramType = $params[0]->getType(); - // getName() is not part of the declared reflection API, but it's there. - // @phpstan-ignore-next-line - $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); - - if ($attrib instanceof ListenerBefore) { - $this->addListenerServiceBefore($attrib->before, $service, $methodName, $type, $attrib->id); - } elseif ($attrib instanceof ListenerAfter) { - $this->addListenerServiceAfter($attrib->after, $service, $methodName, $type, $attrib->id); - } elseif ($attrib instanceof ListenerPriority) { - $this->addListenerService($service, $methodName, $type, $attrib->priority, $attrib->id); - } else { - $this->addListenerService($service, $methodName, $type, null, $attrib->id); - } - } - } elseif (str_starts_with($methodName, 'on')) { - $params = $rMethod->getParameters(); - $type = $params[0]->getType(); - if (is_null($type)) { - throw InvalidTypeException::fromClassCallable($class, $methodName); - } - // getName() is not part of the declared reflection API, but it's there. - // @phpstan-ignore-next-line - $this->addListenerService($service, $rMethod->getName(), $type->getName()); + if (str_starts_with($methodName, 'on') || $attrib) { + $paramType = $params[0]->getType(); + + $id ??= $attrib->id ?? $service . '-' . $methodName; + $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); + + $this->listener($this->makeListenerForService($service, $methodName), $attrib?->order, $id, $type); } } diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 6050c25..0772fa5 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -47,11 +47,30 @@ public function getOptimizedEvents(): array return $this->optimizedEvents; } + public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string + { + $attrib ??= $this->getAttributes($listener)[0] ?? null; + $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); + $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); + $order ??= $attrib?->order; + + $entry = $this->getListenerEntry($listener, $type); + + return match (true) { + $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), + $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), + $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), + default => $this->listeners->addItem($entry, id: $id), + }; + } + + public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { + return $this->listener($listener, $priority ? Order::Priority($priority) : null, $id, $type); + /* if ($attributes = $this->getAttributes($listener)) { // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ foreach ($attributes as $attrib) { $type = $type ?? $attrib->type ?? $this->getType($listener); $id = $id ?? $attrib->id ?? $this->getListenerId($listener); @@ -74,6 +93,7 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ $id ??= $this->getListenerId($listener); return $this->listeners->addItem($entry, $priority ?? 0, $id); + */ } public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string From fd3a2d5b2125818e41b05517ae25348a5a9b32e9 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:08:42 -0500 Subject: [PATCH 12/77] Short closure. --- src/OrderedListenerProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index ac96db9..8bd1391 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -194,8 +194,6 @@ protected function makeListenerForService(string $serviceName, string $methodNam // the wrapping listener must listen to just object. The explicit $type means it will still get only // the right event type, and the real listener can still type itself properly. $container = $this->container; - return static function (object $event) use ($serviceName, $methodName, $container): void { - $container->get($serviceName)->$methodName($event); - }; + return static fn (object $event) => $container->get($serviceName)->$methodName($event); } } From 6c4fc18406c9f3dac185dc62c06838d76ff5619a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:11:57 -0500 Subject: [PATCH 13/77] Remove even more code. --- src/ProviderBuilder.php | 64 ++--------------------------------------- 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 0772fa5..1547997 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -64,79 +64,19 @@ public function listener(callable $listener, ?Order $order = null, ?string $id = }; } - public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { return $this->listener($listener, $priority ? Order::Priority($priority) : null, $id, $type); - /* - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - foreach ($attributes as $attrib) { - $type = $type ?? $attrib->type ?? $this->getType($listener); - $id = $id ?? $attrib->id ?? $this->getListenerId($listener); - $entry = $this->getListenerEntry($listener, $type); - if ($attrib instanceof ListenerBefore) { - $generatedId = $this->listeners->addItemBefore($attrib->before, $entry, $id); - } elseif ($attrib instanceof ListenerAfter) { - $generatedId = $this->listeners->addItemAfter($attrib->after, $entry, $id); - } elseif ($attrib instanceof ListenerPriority) { - $generatedId = $this->listeners->addItem($entry, $attrib->priority, $id); - } else { - $generatedId = $this->listeners->addItem($entry, $priority ?? 0, $id); - } - } - // Return the last id only, because that's all we can do. - return $generatedId; - } - - $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); - $id ??= $this->getListenerId($listener); - - return $this->listeners->addItem($entry, $priority ?? 0, $id); - */ } public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string { - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ - foreach ($attributes as $attrib) { - $type ??= $attrib->type ?? $this->getType($listener); - $id ??= $attrib->id ?? $this->getListenerId($listener); - $entry = $this->getListenerEntry($listener, $type); - // The before-ness of this method takes priority over the attribute. - $generatedId = $this->listeners->addItemBefore($before, $entry, $id); - } - // Return the last id only, because that's all we can do. - return $generatedId; - } - - $id ??= $this->getListenerId($listener); - $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); - return $this->listeners->addItemBefore($before, $entry, $id); + return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); } public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string { - if ($attributes = $this->getAttributes($listener)) { - // @todo We can probably do better than this in the next major. - /** @var Listener|ListenerBefore|ListenerAfter|ListenerPriority $attrib */ - foreach ($attributes as $attrib) { - $type ??= $attrib->type ?? $this->getType($listener); - $id ??= $attrib->id ?? $this->getListenerId($listener); - $entry = $this->getListenerEntry($listener, $type); - // The before-ness of this method takes priority over the attribute. - $generatedId = $this->listeners->addItemBefore($after, $entry, $id); - } - // Return the last id only, because that's all we can do. - return $generatedId; - } - - $entry = $this->getListenerEntry($listener, $type ?? $this->getParameterType($listener)); - $id ??= $this->getListenerId($listener); - - return $this->listeners->addItemAfter($after, $entry, $id); + return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); } public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string From bd0ce47f933448d52323defb98fbc2edc8a2be4f Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:30:25 -0500 Subject: [PATCH 14/77] Fold even more code down together. --- src/OrderedListenerProvider.php | 18 ++++++++++-------- src/ProviderBuilder.php | 24 +++++++++++++++--------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 8bd1391..30e902b 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -84,23 +84,25 @@ public function addListenerAfter(string $after, callable $listener, ?string $id return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); } - public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string + public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string { $id ??= $service . '-' . $method; - $priority ??= 0; - return $this->addListener($this->makeListenerForService($service, $method), $priority, $id, $type); + return $this->listener($this->makeListenerForService($service, $method), $order, $id, $type); + } + + public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string + { + return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); } public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string { - $id ??= $service . '-' . $method; - return $this->addListenerBefore($before, $this->makeListenerForService($service, $method), $id, $type); + return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); } public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string { - $id = $id ?? $service . '-' . $method; - return $this->addListenerAfter($after, $this->makeListenerForService($service, $method), $id, $type); + return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); } public function addSubscriber(string $class, string $service): void @@ -162,7 +164,7 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class if (str_starts_with($methodName, 'on') || $attrib) { $paramType = $params[0]->getType(); - $id ??= $attrib->id ?? $service . '-' . $methodName; + $id = $attrib->id ?? $service . '-' . $methodName; $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); $this->listener($this->makeListenerForService($service, $methodName), $attrib?->order, $id, $type); diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 1547997..d8bd796 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -79,26 +79,32 @@ public function addListenerAfter(string $after, callable $listener, ?string $id return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); } - public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string + public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string { $entry = new ListenerServiceEntry($service, $method, $type); - $priority ??= 0; + $id ??= $service . '-' . $method; - return $this->listeners->addItem($entry, $priority, $id); + return match (true) { + $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), + $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), + $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), + default => $this->listeners->addItem($entry, id: $id), + }; } - public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string + public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string { - $entry = new ListenerServiceEntry($service, $method, $type); + return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); + } - return $this->listeners->addItemBefore($before, $entry, $id); + public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string + { + return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); } public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string { - $entry = new ListenerServiceEntry($service, $method, $type); - - return $this->listeners->addItemAfter($after, $entry, $id); + return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); } public function addSubscriber(string $class, string $service): void From 7d146dcd40684bad2b6e2a42263d5dff41adb4af Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:51:41 -0500 Subject: [PATCH 15/77] Remove test for edge case feature we're no longer supporting. --- .../OrderedListenerProviderAttributeTest.php | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index 2be9933..5915d4c 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -191,25 +191,4 @@ public function test_before_after_methods_win_over_attributes(): void $this->assertEquals('CAD', implode($event->result())); } - - public function test_multiple_attributes_read_separately(): void - { - $this->markTestSkipped('We are probably removing this functionality.'); - /* - $p = new OrderedListenerProvider(); - - // Just to make the following lines shorter and easier to read. - $ns = '\\Crell\\Tukio\\'; - - $idOne = $p->addListener("{$ns}at_multi_one"); - - $event = new CollectingEvent(); - - foreach ($p->getListenersForEvent($event) as $listener) { - $listener($event); - } - - $this->assertEquals('AAA', implode($event->result())); - */ - } } From 27a7acde2cfc630fe69c8090d909a770a7ae1f89 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 16:57:44 -0500 Subject: [PATCH 16/77] Refactor most of the provider and provider builder into a common base class. Sad panda for using abstract classes. --- src/Entry/ListenerStaticMethodEntry.php | 1 - src/OrderedListenerProvider.php | 132 +---------------------- src/OrderedProviderInterface.php | 5 + src/ProviderBuilder.php | 93 +--------------- src/ProviderCollector.php | 138 ++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 222 deletions(-) create mode 100644 src/ProviderCollector.php diff --git a/src/Entry/ListenerStaticMethodEntry.php b/src/Entry/ListenerStaticMethodEntry.php index 15611b4..4ed00df 100644 --- a/src/Entry/ListenerStaticMethodEntry.php +++ b/src/Entry/ListenerStaticMethodEntry.php @@ -22,7 +22,6 @@ public function __construct(string $class, string $method, string $type) $this->class = $class; $this->method = $method; $this->type = $type; - $this->listener = [$class, $method]; } /** diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 30e902b..619db5b 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -4,17 +4,13 @@ namespace Crell\Tukio; -use Crell\Tukio\Entry\ListenerEntry; use Crell\OrderedCollection\OrderedCollection; -use Crell\Tukio\Entry\ListenerFunctionEntry; -use Crell\Tukio\Entry\ListenerStaticMethodEntry; +use Crell\Tukio\Entry\ListenerEntry; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\ListenerProviderInterface; -class OrderedListenerProvider implements ListenerProviderInterface, OrderedProviderInterface +class OrderedListenerProvider extends ProviderCollector implements ListenerProviderInterface { - use ProviderUtilities; - /** * @var OrderedCollection */ @@ -22,7 +18,7 @@ class OrderedListenerProvider implements ListenerProviderInterface, OrderedProvi public function __construct(protected ?ContainerInterface $container = null) { - $this->listeners = new OrderedCollection(); + parent::__construct(); } /** @@ -38,139 +34,17 @@ public function getListenersForEvent(object $event): iterable } } - public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string - { - $attrib ??= $this->getAttributes($listener)[0] ?? null; - $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); - $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); - $order ??= $attrib?->order; - - $entry = $this->getListenerEntry($listener, $type); - - return match (true) { - $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), - $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), - $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), - default => $this->listeners->addItem($entry, id: $id), - }; - } - protected function getListenerEntry(callable $listener, string $type): ListenerEntry { - // String means it's a function name, and that's safe. - if (is_string($listener)) { - return new ListenerFunctionEntry($listener, $type); - } - // This is how we recognize a static method call. - if (is_array($listener) && isset($listener[0]) && is_string($listener[0])) { - return new ListenerStaticMethodEntry($listener[0], $listener[1], $type); - } - return new ListenerEntry($listener, $type); } - public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, ($priority !== null) ? Order::Priority($priority) : null, $id, $type); - } - - public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); - } - - public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); - } - public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string { $id ??= $service . '-' . $method; return $this->listener($this->makeListenerForService($service, $method), $order, $id, $type); } - public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); - } - - public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); - } - - public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); - } - - public function addSubscriber(string $class, string $service): void - { - $proxy = $this->addSubscribersByProxy($class, $service); - - try { - $methods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); - - $methods = array_filter($methods, fn(\ReflectionMethod $r) - => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); - - /** @var \ReflectionMethod $rMethod */ - foreach ($methods as $rMethod) { - $this->addSubscriberMethod($rMethod, $class, $service); - } - } catch (\ReflectionException $e) { - throw new \RuntimeException('Type error registering subscriber.', 0, $e); - } - } - - protected function addSubscribersByProxy(string $class, string $service): ListenerProxy - { - $proxy = new ListenerProxy($this, $service, $class); - - // Explicit registration is opt-in. - if (in_array(SubscriberInterface::class, class_implements($class), true)) { - /** @var SubscriberInterface $class */ - $class::registerListeners($proxy); - } - return $proxy; - } - - /** - * @return array - */ - protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array - { - $attributes = array_map(static fn (\ReflectionAttribute $attrib): object - => $attrib->newInstance(), $rMethod->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF)); - - return $attributes; - } - - protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class, string $service): void - { - $methodName = $rMethod->getName(); - $params = $rMethod->getParameters(); - - if (count($params) < 1) { - // Skip this method, as it doesn't take arguments. - return; - } - - $attributes = $this->findAttributesOnMethod($rMethod); - /** @var Listener $attrib */ - $attrib = $attributes[0] ?? null; - - if (str_starts_with($methodName, 'on') || $attrib) { - $paramType = $params[0]->getType(); - - $id = $attrib->id ?? $service . '-' . $methodName; - $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); - - $this->listener($this->makeListenerForService($service, $methodName), $attrib?->order, $id, $type); - } - } - /** * Creates a callable that will proxy to the provided service and method. * diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 02418de..5920027 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -6,6 +6,11 @@ interface OrderedProviderInterface { + + public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string; + + public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string; + /** * Adds a listener to the provider. * diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index d8bd796..23f091e 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -8,27 +8,14 @@ use Crell\Tukio\Entry\ListenerFunctionEntry; use Crell\Tukio\Entry\ListenerServiceEntry; use Crell\Tukio\Entry\ListenerStaticMethodEntry; -use Crell\OrderedCollection\OrderedCollection; -class ProviderBuilder implements OrderedProviderInterface, \IteratorAggregate +class ProviderBuilder extends ProviderCollector implements \IteratorAggregate { - use ProviderUtilities; - - /** - * @var OrderedCollection - */ - protected OrderedCollection $listeners; - /** * @var array */ protected array $optimizedEvents = []; - public function __construct() - { - $this->listeners = new OrderedCollection(); - } - /** * Pre-specify an event class that should have an optimized listener list built. * @@ -47,38 +34,6 @@ public function getOptimizedEvents(): array return $this->optimizedEvents; } - public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string - { - $attrib ??= $this->getAttributes($listener)[0] ?? null; - $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); - $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); - $order ??= $attrib?->order; - - $entry = $this->getListenerEntry($listener, $type); - - return match (true) { - $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), - $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), - $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), - default => $this->listeners->addItem($entry, id: $id), - }; - } - - public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, $priority ? Order::Priority($priority) : null, $id, $type); - } - - public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); - } - - public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string - { - return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); - } - public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string { $entry = new ListenerServiceEntry($service, $method, $type); @@ -92,52 +47,6 @@ public function listenerService(string $service, string $method, string $type, ? }; } - public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); - } - - public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); - } - - public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string - { - return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); - } - - public function addSubscriber(string $class, string $service): void - { - // @todo This method is identical to the one in OrderedListenerProvider. Is it worth merging them? - - $proxy = new ListenerProxy($this, $service, $class); - - // Explicit registration is opt-in. - if (in_array(SubscriberInterface::class, class_implements($class), true)) { - /** @var SubscriberInterface $class */ - $class::registerListeners($proxy); - } - - try { - $rClass = new \ReflectionClass($class); - $methods = $rClass->getMethods(\ReflectionMethod::IS_PUBLIC); - /** @var \ReflectionMethod $rMethod */ - foreach ($methods as $rMethod) { - $methodName = $rMethod->getName(); - if (str_starts_with($methodName, 'on') && !in_array($methodName, $proxy->getRegisteredMethods(), true)) { - $params = $rMethod->getParameters(); - // getName() is not part of the declared reflection API, but it's there. - // @phpstan-ignore-next-line - $type = $params[0]->getType()->getName(); - $this->addListenerService($service, $rMethod->getName(), $type); - } - } - } catch (\ReflectionException $e) { - throw new \RuntimeException('Type error registering subscriber.', 0, $e); - } - } - public function getIterator(): \Traversable { yield from $this->listeners; diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php new file mode 100644 index 0000000..aefdc8d --- /dev/null +++ b/src/ProviderCollector.php @@ -0,0 +1,138 @@ + + */ + protected OrderedCollection $listeners; + + public function __construct() + { + $this->listeners = new OrderedCollection(); + } + + public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string + { + $attrib ??= $this->getAttributes($listener)[0] ?? null; + $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); + $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); + $order ??= $attrib?->order; + + $entry = $this->getListenerEntry($listener, $type); + + return match (true) { + $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), + $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), + $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), + default => $this->listeners->addItem($entry, id: $id), + }; + } + + public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string + { + return $this->listener($listener, $priority ? Order::Priority($priority) : null, $id, $type); + } + + public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string + { + return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); + } + + public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string + { + return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); + } + + public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string + { + return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); + } + + public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string + { + return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); + } + + public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string + { + return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); + } + + public function addSubscriber(string $class, string $service): void + { + $proxy = $this->addSubscribersByProxy($class, $service); + + try { + $methods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); + + $methods = array_filter($methods, static fn(\ReflectionMethod $r) + => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); + + /** @var \ReflectionMethod $rMethod */ + foreach ($methods as $rMethod) { + $this->addSubscriberMethod($rMethod, $class, $service); + } + } catch (\ReflectionException $e) { + throw new \RuntimeException('Type error registering subscriber.', 0, $e); + } + } + + protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class, string $service): void + { + $methodName = $rMethod->getName(); + $params = $rMethod->getParameters(); + + if (count($params) < 1) { + // Skip this method, as it doesn't take arguments. + return; + } + + $attributes = $this->findAttributesOnMethod($rMethod); + /** @var Listener $attrib */ + $attrib = $attributes[0] ?? null; + + if (str_starts_with($methodName, 'on') || $attrib) { + $paramType = $params[0]->getType(); + + $id = $attrib->id ?? $service . '-' . $methodName; + $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); + + $this->listenerService($service, $methodName, $type, $attrib?->order, $id); + } + } + + /** + * @return array + */ + protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array + { + $attributes = array_map(static fn (\ReflectionAttribute $attrib): object + => $attrib->newInstance(), $rMethod->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF)); + + return $attributes; + } + + protected function addSubscribersByProxy(string $class, string $service): ListenerProxy + { + $proxy = new ListenerProxy($this, $service, $class); + + // Explicit registration is opt-in. + if (in_array(SubscriberInterface::class, class_implements($class), true)) { + /** @var SubscriberInterface $class */ + $class::registerListeners($proxy); + } + return $proxy; + } + + abstract protected function getListenerEntry(callable $listener, string $type): ListenerEntry; +} From 3aaafbf52aa54ef3561b16ee5594d1c37e2ac371 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:08:45 -0500 Subject: [PATCH 17/77] Doc improvements. --- src/OrderedProviderInterface.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 5920027..9623728 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -7,8 +7,40 @@ interface OrderedProviderInterface { + /** + * Adds a listener to the provider. + * + * @param callable $listener + * The listener to register. + * @param Order|null $order + * One of Order::Priority(), Order::Before(), or Order::After(). + * @param ?string $id + * The identifier by which this listener should be known. If not specified one will be generated. + * @param ?string $type + * The class or interface type of events for which this listener will be registered. If not provided + * it will be derived based on the type hint of the listener. + * + * @return string + * The opaque ID of the listener. This can be used for future reference. */ public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string; + /** + * Adds a method on a service as a listener. + * + * This method does not support attributes, as the class name is unknown at registration. + * + * @param string $service + * The name of a service on which this listener lives. + * @param string $method + * The method name of the service that is the listener being registered. + * @param string $type + * The class or interface type of events for which this listener will be registered. * @param Order|null $order + * @param Order|null $order + * One of Order::Priority(), Order::Before(), or Order::After(). + * + * @return string + * The opaque ID of the listener. This can be used for future reference. + */ public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string; /** From 0dbe353febcfebd4f92e1a516a7aa3ab4bd6d602 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:10:30 -0500 Subject: [PATCH 18/77] Improve variable names to be more standardized. --- src/ListenerProxy.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ListenerProxy.php b/src/ListenerProxy.php index 86405e1..e562274 100644 --- a/src/ListenerProxy.php +++ b/src/ListenerProxy.php @@ -57,7 +57,7 @@ public function addListener(string $methodName, ?int $priority = 0, ?string $id * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made * regarding when it comes relative to any other listener. * - * @param string $pivotId + * @param string $before * The ID of an existing listener. * @param string $methodName * The method name of the service that is the listener being registered. @@ -69,11 +69,11 @@ public function addListener(string $methodName, ?int $priority = 0, ?string $id * @return string * The opaque ID of the listener. This can be used for future reference. */ - public function addListenerBefore(string $pivotId, string $methodName, ?string $id = null, ?string $type = null): string + public function addListenerBefore(string $before, string $methodName, ?string $id = null, ?string $type = null): string { $type = $type ?? $this->getServiceMethodType($methodName); $this->registeredMethods[] = $methodName; - return $this->provider->addListenerServiceBefore($pivotId, $this->serviceName, $methodName, $type, $id); + return $this->provider->addListenerServiceBefore($before, $this->serviceName, $methodName, $type, $id); } /** @@ -82,7 +82,7 @@ public function addListenerBefore(string $pivotId, string $methodName, ?string $ * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made * regarding when it comes relative to any other listener. * - * @param string $pivotId + * @param string $after * The ID of an existing listener. * @param string $methodName * The method name of the service that is the listener being registered. @@ -94,11 +94,11 @@ public function addListenerBefore(string $pivotId, string $methodName, ?string $ * @return string * The opaque ID of the listener. This can be used for future reference. */ - public function addListenerAfter(string $pivotId, string $methodName, ?string $id = null, ?string $type = null): string + public function addListenerAfter(string $after, string $methodName, ?string $id = null, ?string $type = null): string { $type = $type ?? $this->getServiceMethodType($methodName); $this->registeredMethods[] = $methodName; - return $this->provider->addListenerServiceAfter($pivotId, $this->serviceName, $methodName, $type, $id); + return $this->provider->addListenerServiceAfter($after, $this->serviceName, $methodName, $type, $id); } /** From 234afd3997a0f80b25a9ef9efbb970165b082e7d Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:13:54 -0500 Subject: [PATCH 19/77] More PHP 8 syntax. --- src/ProviderCompiler.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 89e283f..d0c4f79 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -102,19 +102,14 @@ protected function endOptimizedEntry(): string protected function createOptimizedEntry(CompileableListenerEntryInterface $listenerEntry): string { $listener = $listenerEntry->getProperties(); - switch ($listener['entryType']) { - case ListenerFunctionEntry::class: - $ret = "'{$listener['listener']}'"; - break; - case ListenerStaticMethodEntry::class: - $ret = var_export([$listener['class'], $listener['method']], true); - break; - case ListenerServiceEntry::class: - $ret = sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', $listener['serviceName'], $listener['method']); - break; - default: - throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); - } + $ret = match ($listener['entryType']) { + ListenerFunctionEntry::class => "'{$listener['listener']}'", + ListenerStaticMethodEntry::class => var_export([$listener['class'], $listener['method']], true), + ListenerServiceEntry::class => sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', + $listener['serviceName'], $listener['method']), + default => throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', + $listener['entryType'])), + }; return $ret . ',' . PHP_EOL; } From 0b49129e2519296def37e914bfefb22e241bd02a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:15:56 -0500 Subject: [PATCH 20/77] More PHP 8 syntax. --- src/CompiledListenerProviderBase.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index ad60610..bf6d6f9 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -9,8 +9,6 @@ class CompiledListenerProviderBase implements ListenerProviderInterface { - protected ContainerInterface $container; - // This nested array will be generated by the compiler in a subclass. It's listed here for reference only. // Its structure is an ordered list of array definitions, each of which corresponds to one of the defined // entry types in the classes seen in getListenerForEvent(). See each class's getProperties() method for the @@ -21,10 +19,7 @@ class CompiledListenerProviderBase implements ListenerProviderInterface /** @var array */ protected array $optimized = []; - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } + public function __construct(protected ContainerInterface $container) {} /** * @return iterable From eed673f17fed8e35d7cea4c34d58c596bf3d94c6 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:18:01 -0500 Subject: [PATCH 21/77] Yet more PHP 8 syntax. --- src/Entry/ListenerServiceEntry.php | 17 +++++------------ src/Entry/ListenerStaticMethodEntry.php | 17 +++++------------ 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/Entry/ListenerServiceEntry.php b/src/Entry/ListenerServiceEntry.php index db35f41..d188086 100644 --- a/src/Entry/ListenerServiceEntry.php +++ b/src/Entry/ListenerServiceEntry.php @@ -11,18 +11,11 @@ */ class ListenerServiceEntry implements CompileableListenerEntryInterface { - public string $serviceName; - - public string $method; - - public string $type; - - public function __construct(string $serviceName, string $method, string $type) - { - $this->serviceName = $serviceName; - $this->method = $method; - $this->type = $type; - } + public function __construct( + public string $serviceName, + public string $method, + public string $type, + ) {} /** * @return array{ diff --git a/src/Entry/ListenerStaticMethodEntry.php b/src/Entry/ListenerStaticMethodEntry.php index 4ed00df..1374f3d 100644 --- a/src/Entry/ListenerStaticMethodEntry.php +++ b/src/Entry/ListenerStaticMethodEntry.php @@ -11,18 +11,11 @@ */ class ListenerStaticMethodEntry extends ListenerEntry implements CompileableListenerEntryInterface { - public string $class; - - public string $method; - - public string $type; - - public function __construct(string $class, string $method, string $type) - { - $this->class = $class; - $this->method = $method; - $this->type = $type; - } + public function __construct( + public string $class, + public string $method, + public string $type, + ) {} /** * @return array{ From 12c76964a4127b34dd421437cd413c80c97905df Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:20:02 -0500 Subject: [PATCH 22/77] Eliminate a trait that we no longer need. --- src/ProviderCollector.php | 135 +++++++++++++++++++++++++++++++++- src/ProviderUtilities.php | 149 -------------------------------------- 2 files changed, 134 insertions(+), 150 deletions(-) delete mode 100644 src/ProviderUtilities.php diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index aefdc8d..67af856 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -6,10 +6,11 @@ use Crell\OrderedCollection\OrderedCollection; use Crell\Tukio\Entry\ListenerEntry; +use Fig\EventDispatcher\ParameterDeriverTrait; abstract class ProviderCollector implements OrderedProviderInterface { - use ProviderUtilities; + use ParameterDeriverTrait; /** * @var OrderedCollection @@ -134,5 +135,137 @@ protected function addSubscribersByProxy(string $class, string $service): Listen return $proxy; } + /** + * @return array + */ + protected function getAttributes(callable $listener): array + { + $ref = null; + + if ($this->isFunctionCallable($listener)) { + $ref = new \ReflectionFunction($listener); + } elseif ($this->isClassCallable($listener)) { + // PHPStan says you cannot use array destructuring on a callable, but you can + // if you know that it's an array (which in context we do). + // @phpstan-ignore-next-line + [$class, $method] = $listener; + $ref = (new \ReflectionClass($class))->getMethod($method); + } elseif ($this->isObjectCallable($listener)) { + // PHPStan says you cannot use array destructuring on a callable, but you can + // if you know that it's an array (which in context we do). + // @phpstan-ignore-next-line + [$class, $method] = $listener; + $ref = (new \ReflectionObject($class))->getMethod($method); + } + + if (!$ref) { + return []; + } + + $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF); + + return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); + } + + /** + * Tries to get the type of a callable listener. + * + * If unable, throws an exception with information about the listener whose type could not be fetched. + * + * @param callable $listener + * The callable from which to extract a type. + * + * @return string + * The type of the first argument. + */ + protected function getType(callable $listener): string + { + try { + $type = $this->getParameterType($listener); + } catch (\InvalidArgumentException $exception) { + if ($this->isClassCallable($listener) || $this->isObjectCallable($listener)) { + throw InvalidTypeException::fromClassCallable($listener[0], $listener[1], $exception); + } + if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { + throw InvalidTypeException::fromFunctionCallable($listener, $exception); + } + throw new InvalidTypeException($exception->getMessage(), $exception->getCode(), $exception); + } + return $type; + } + + /** + * Derives a predictable ID from the listener if possible. + * + * It's OK for this method to return null, as OrderedCollection will + * generate a random ID if necessary. It will also handle duplicates + * for us. This method is just a suggestion. + * + * @param callable $listener + * The listener for which to derive an ID. + * + * @return string|null + * The derived ID if possible or null if no reasonable ID could be derived. + */ + protected function getListenerId(callable $listener): ?string + { + // The methods called in this method are from an external trait, and + // its docblock is a bit buggy. Just ignore that on our end until + // it's fixed in the util package. + // @phpstan-ignore-next-line + if ($this->isFunctionCallable($listener)) { + // Function callables are strings, so use that directly. + // @phpstan-ignore-next-line + return (string)$listener; + } + // @phpstan-ignore-next-line + if ($this->isClassCallable($listener)) { + return $listener[0] . '::' . $listener[1]; + } + // @phpstan-ignore-next-line + if (is_array($listener) && is_object($listener[0])) { + return get_class($listener[0]) . '::' . $listener[1]; + } + + // Anything else we can't derive an ID for logically. + return null; + } + + /** + * Determines if a callable represents a function. + * + * Or at least a reasonable approximation, since a function name may not be defined yet. + * + * @return bool + * True if the callable represents a function, false otherwise. + */ + protected function isFunctionCallable(callable $callable): bool + { + // We can't check for function_exists() because it may be included later by the time it matters. + return is_string($callable); + } + + /** + * Determines if a callable represents a method on an object. + * + * @return bool + * True if the callable represents a method object, false otherwise. + */ + protected function isObjectCallable(callable $callable): bool + { + return is_array($callable) && is_object($callable[0]); + } + + /** + * Determines if a callable represents a closure/anonymous function. + * + * @return bool + * True if the callable represents a closure object, false otherwise. + */ + protected function isClosureCallable(callable $callable): bool + { + return $callable instanceof \Closure; + } + abstract protected function getListenerEntry(callable $listener, string $type): ListenerEntry; } diff --git a/src/ProviderUtilities.php b/src/ProviderUtilities.php deleted file mode 100644 index d114996..0000000 --- a/src/ProviderUtilities.php +++ /dev/null @@ -1,149 +0,0 @@ - - */ - protected function getAttributes(callable $listener): array - { - $ref = null; - - if ($this->isFunctionCallable($listener)) { - $ref = new \ReflectionFunction($listener); - } elseif ($this->isClassCallable($listener)) { - // PHPStan says you cannot use array destructuring on a callable, but you can - // if you know that it's an array (which in context we do). - // @phpstan-ignore-next-line - [$class, $method] = $listener; - $ref = (new \ReflectionClass($class))->getMethod($method); - } elseif ($this->isObjectCallable($listener)) { - // PHPStan says you cannot use array destructuring on a callable, but you can - // if you know that it's an array (which in context we do). - // @phpstan-ignore-next-line - [$class, $method] = $listener; - $ref = (new \ReflectionObject($class))->getMethod($method); - } - - if (!$ref) { - return []; - } - - $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF); - - return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); - } - - /** - * Tries to get the type of a callable listener. - * - * If unable, throws an exception with information about the listener whose type could not be fetched. - * - * @param callable $listener - * The callable from which to extract a type. - * - * @return string - * The type of the first argument. - */ - protected function getType(callable $listener): string - { - try { - $type = $this->getParameterType($listener); - } catch (\InvalidArgumentException $exception) { - if ($this->isClassCallable($listener) || $this->isObjectCallable($listener)) { - throw InvalidTypeException::fromClassCallable($listener[0], $listener[1], $exception); - } - if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { - throw InvalidTypeException::fromFunctionCallable($listener, $exception); - } - throw new InvalidTypeException($exception->getMessage(), $exception->getCode(), $exception); - } - return $type; - } - - /** - * Derives a predictable ID from the listener if possible. - * - * It's OK for this method to return null, as OrderedCollection will - * generate a random ID if necessary. It will also handle duplicates - * for us. This method is just a suggestion. - * - * @param callable $listener - * The listener for which to derive an ID. - * - * @return string|null - * The derived ID if possible or null if no reasonable ID could be derived. - */ - protected function getListenerId(callable $listener): ?string - { - // The methods called in this method are from an external trait, and - // its docblock is a bit buggy. Just ignore that on our end until - // it's fixed in the util package. - // @phpstan-ignore-next-line - if ($this->isFunctionCallable($listener)) { - // Function callables are strings, so use that directly. - // @phpstan-ignore-next-line - return (string)$listener; - } - // @phpstan-ignore-next-line - if ($this->isClassCallable($listener)) { - return $listener[0] . '::' . $listener[1]; - } - // @phpstan-ignore-next-line - if (is_array($listener) && is_object($listener[0])) { - return get_class($listener[0]) . '::' . $listener[1]; - } - - // Anything else we can't derive an ID for logically. - return null; - } - - /** - * Determines if a callable represents a function. - * - * Or at least a reasonable approximation, since a function name may not be defined yet. - * - * @return bool - * True if the callable represents a function, false otherwise. - */ - protected function isFunctionCallable(callable $callable): bool - { - // We can't check for function_exists() because it may be included later by the time it matters. - return is_string($callable); - } - - /** - * Determines if a callable represents a method on an object. - * - * @return bool - * True if the callable represents a method object, false otherwise. - */ - protected function isObjectCallable(callable $callable): bool - { - return is_array($callable) && is_object($callable[0]); - } - - /** - * Determines if a callable represents a closure/anonymous function. - * - * @return bool - * True if the callable represents a closure object, false otherwise. - */ - protected function isClosureCallable(callable $callable): bool - { - return $callable instanceof \Closure; - } -} From 015439554a3bdcb1d1784e529eb478872de10ed4 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:24:25 -0500 Subject: [PATCH 23/77] PHPStan fixes. --- src/Order.php | 6 +++--- src/ProviderCollector.php | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Order.php b/src/Order.php index 1259db1..13d74fe 100644 --- a/src/Order.php +++ b/src/Order.php @@ -9,17 +9,17 @@ */ abstract class Order { - public static function Priority(int $priority): static + public static function Priority(int $priority): OrderPriority { return new OrderPriority(priority: $priority); } - public static function Before(string $before): static + public static function Before(string $before): OrderBefore { return new OrderBefore(before: $before); } - public static function After(string $after): static + public static function After(string $after): OrderAfter { return new OrderAfter(after: $after); } diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 67af856..0c7fcd3 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -24,7 +24,7 @@ public function __construct() public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string { - $attrib ??= $this->getAttributes($listener)[0] ?? null; + $attrib = $this->getAttributes($listener)[0] ?? null; $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); $order ??= $attrib?->order; @@ -99,13 +99,15 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class } $attributes = $this->findAttributesOnMethod($rMethod); - /** @var Listener $attrib */ + /** @var ?Listener $attrib */ $attrib = $attributes[0] ?? null; if (str_starts_with($methodName, 'on') || $attrib) { $paramType = $params[0]->getType(); $id = $attrib->id ?? $service . '-' . $methodName; + // getName() is not a documented part of the Reflection API, but it's always there. + // @phpstan-ignore-next-line $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); $this->listenerService($service, $methodName, $type, $attrib?->order, $id); @@ -209,20 +211,14 @@ protected function getType(callable $listener): string */ protected function getListenerId(callable $listener): ?string { - // The methods called in this method are from an external trait, and - // its docblock is a bit buggy. Just ignore that on our end until - // it's fixed in the util package. - // @phpstan-ignore-next-line if ($this->isFunctionCallable($listener)) { // Function callables are strings, so use that directly. // @phpstan-ignore-next-line return (string)$listener; } - // @phpstan-ignore-next-line if ($this->isClassCallable($listener)) { return $listener[0] . '::' . $listener[1]; } - // @phpstan-ignore-next-line if (is_array($listener) && is_object($listener[0])) { return get_class($listener[0]) . '::' . $listener[1]; } From 2c24dd41332d1fa2730ca39368b31476a7e896b7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:25:13 -0500 Subject: [PATCH 24/77] Remove Docker setups for old PHP versions. --- docker/php/74/Dockerfile | 8 -------- docker/php/74/xdebug.ini | 2 -- docker/php/80/Dockerfile | 8 -------- docker/php/80/xdebug.ini | 2 -- 4 files changed, 20 deletions(-) delete mode 100644 docker/php/74/Dockerfile delete mode 100644 docker/php/74/xdebug.ini delete mode 100644 docker/php/80/Dockerfile delete mode 100644 docker/php/80/xdebug.ini diff --git a/docker/php/74/Dockerfile b/docker/php/74/Dockerfile deleted file mode 100644 index 10cdff0..0000000 --- a/docker/php/74/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM php:7.4-cli -WORKDIR /usr/src/myapp - -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -RUN apt-get update && apt-get install zip unzip git -y \ - && pecl install xdebug \ - && pecl install pcov diff --git a/docker/php/74/xdebug.ini b/docker/php/74/xdebug.ini deleted file mode 100644 index b1ec307..0000000 --- a/docker/php/74/xdebug.ini +++ /dev/null @@ -1,2 +0,0 @@ -zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so -xdebug.output_dir=profiles diff --git a/docker/php/80/Dockerfile b/docker/php/80/Dockerfile deleted file mode 100644 index ae37e01..0000000 --- a/docker/php/80/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM php:8.0-cli -WORKDIR /usr/src/myapp - -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -RUN apt-get update && apt-get install zip unzip git -y \ - && pecl install xdebug \ - && pecl install pcov diff --git a/docker/php/80/xdebug.ini b/docker/php/80/xdebug.ini deleted file mode 100644 index d2090a9..0000000 --- a/docker/php/80/xdebug.ini +++ /dev/null @@ -1,2 +0,0 @@ -zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20210902/xdebug.so -xdebug.output_dir=profiles From 0cb237dc2b595cf2df6997521a7cc167f8c80bb1 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 16 Sep 2023 17:30:37 -0500 Subject: [PATCH 25/77] Doc improvements. --- src/Listener.php | 13 +++++++++---- src/OrderedProviderInterface.php | 12 ++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index d37f8bd..9851577 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -8,14 +8,19 @@ /** * The main attribute to customize a listener. - * - * This attribute handles both priority sorting and topological (before/after) - * sorting. For that reason, it MUST always be used with named arguments. - * Specifying more than one of $priority, $before, or $after is an error. */ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class Listener implements ListenerAttribute { + /** + * @param Order|null $order + * One of Order::Priority(), Order::Before(), or Order::After(). + * @param ?string $id + * The identifier by which this listener should be known. If not specified one will be generated. + * @param ?string $type + * The class or interface type of events for which this listener will be registered. If not provided + * it will be derived based on the type declaration of the listener. + */ public function __construct( public ?string $id = null, public ?Order $order = null, diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 9623728..03609a6 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -18,7 +18,7 @@ interface OrderedProviderInterface * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type * The class or interface type of events for which this listener will be registered. If not provided - * it will be derived based on the type hint of the listener. + * it will be derived based on the type declaration of the listener. * * @return string * The opaque ID of the listener. This can be used for future reference. */ @@ -34,7 +34,7 @@ public function listener(callable $listener, ?Order $order = null, ?string $id = * @param string $method * The method name of the service that is the listener being registered. * @param string $type - * The class or interface type of events for which this listener will be registered. * @param Order|null $order + * The class or interface type of events for which this listener will be registered. * @param Order|null $order * One of Order::Priority(), Order::Before(), or Order::After(). * @@ -57,7 +57,7 @@ public function listenerService(string $service, string $method, string $type, ? * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type * The class or interface type of events for which this listener will be registered. If not provided - * it will be derived based on the type hint of the listener. + * it will be derived based on the type declaration of the listener. * * @return string * The opaque ID of the listener. This can be used for future reference. @@ -81,7 +81,7 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type * The class or interface type of events for which this listener will be registered. If not provided - * it will be derived based on the type hint of the listener. + * it will be derived based on the type declaration of the listener. * * @return string * The opaque ID of the listener. This can be used for future reference. @@ -105,7 +105,7 @@ public function addListenerBefore(string $before, callable $listener, ?string $i * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type * The class or interface type of events for which this listener will be registered. If not provided - * it will be derived based on the type hint of the listener. + * it will be derived based on the type declaration of the listener. * * @return string * The opaque ID of the listener. This can be used for future reference. @@ -189,7 +189,7 @@ public function addListenerServiceAfter(string $after, string $service, string $ * - It's name is in the form on*. onUpdate(), onUserLogin(), onHammerTime() will all be registered. * - It has a Listener/ListenerBefore/ListenerAfter attribute. * - * The event type the listener is for will be derived from the type hint in the method signature. + * The event type the listener is for will be derived from the type declaration in the method signature. * * @param string $class * The class name to be registered as a subscriber. From 3fe59ea1f8847ee150ad381ad86ed937a3b58c74 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 18:41:34 -0500 Subject: [PATCH 26/77] Fix test class names. --- tests/CompiledListenerProviderAttributeTest.php | 2 +- tests/CompiledListenerProviderTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 0e92f3e..0671d42 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -44,7 +44,7 @@ public static function listen(CollectingEvent $event): void } } -class CompiledEventDispatcherAttributeTest extends TestCase +class CompiledListenerProviderAttributeTest extends TestCase { use MakeCompiledProviderTrait; diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index d919f62..d014d72 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -40,7 +40,7 @@ public static function listen(CollectingEvent $event): void } } -class CompiledEventDispatcherTest extends TestCase +class CompiledListenerProviderTest extends TestCase { use MakeCompiledProviderTrait; From d29fc5443262bcc9d37fd8afa5e4fe3022acdd9a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 18:42:55 -0500 Subject: [PATCH 27/77] Upgrade PHPUnit to v10. --- .gitignore | 1 + composer.json | 4 ++-- phpunit.xml.dist | 21 ++++++--------------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 16ab93b..aa5090d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ build composer.lock vendor .phpunit.result.cache +.phpunit.cache /vendor-bin/**/vendor .env diff --git a/composer.json b/composer.json index 808d18b..cb92af9 100644 --- a/composer.json +++ b/composer.json @@ -27,8 +27,8 @@ }, "require-dev": { "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.3" }, "provide": { "psr/event-dispatcher-implementation": "1.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f03fa05..752d521 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,11 @@ - + tests - - src/ - @@ -29,4 +15,9 @@ + + + src/ + + From 1b2a15ec44c30dc9e2f0d6ca44743d1538ecbb2e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 18:43:59 -0500 Subject: [PATCH 28/77] Test on PHP 8.3. --- .github/workflows/phpstan.yaml | 2 +- .github/workflows/testing.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml index e4c15ae..152e65e 100644 --- a/.github/workflows/phpstan.yaml +++ b/.github/workflows/phpstan.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '8.1', '8.2' ] + php: [ '8.1', '8.2', '8.3' ] composer-flags: [ '' ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 44fe829..5a80a1f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '8.1', '8.2' ] + php: [ '8.1', '8.2', '8.3' ] composer-flags: [ '' ] phpunit-flags: [ '--coverage-text' ] steps: From 3b9dd26dbe455687bc5c3ce3ab8f564402783e88 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 18:44:26 -0500 Subject: [PATCH 29/77] Don't double-run tests. --- .github/workflows/phpstan.yaml | 4 +++- .github/workflows/testing.yaml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml index 152e65e..a6debc3 100644 --- a/.github/workflows/phpstan.yaml +++ b/.github/workflows/phpstan.yaml @@ -1,7 +1,9 @@ --- name: PHPStan checks on: - push: ~ + push: + branches: + - master pull_request: ~ jobs: diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 5a80a1f..5b76332 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -1,7 +1,9 @@ --- name: PHPUnit tests on: - push: ~ + push: + branches: + - master pull_request: ~ jobs: From 277117d273180db5489ad0ea67a1608eaf8e443e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 19:07:47 -0500 Subject: [PATCH 30/77] Doc improvements. --- src/CompiledListenerProviderBase.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index bf6d6f9..4c37ee1 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -9,14 +9,25 @@ class CompiledListenerProviderBase implements ListenerProviderInterface { - // This nested array will be generated by the compiler in a subclass. It's listed here for reference only. - // Its structure is an ordered list of array definitions, each of which corresponds to one of the defined - // entry types in the classes seen in getListenerForEvent(). See each class's getProperties() method for the - // exact structure. - /** @var array */ + /** + * This nested array will be generated by the compiler in a subclass. It's listed here for reference only. + * + * The structure is an ordered list of array definitions, each of which corresponds to one of the defined + * entry types in the classes seen in getListenerForEvent(). See each class's getProperties() method for the + * exact structure. + * + * @var array + */ protected array $listeners = []; - /** @var array */ + /** + * This nested array will be generated by the compiler in a subclass. It's listed here for reference only. + * + * The keys are an event class name. The value is an array of callables that may be + * returned to the dispatcher as-is. + * + * @var array + */ protected array $optimized = []; public function __construct(protected ContainerInterface $container) {} From c4a52d2ac86314e4eb483d14d0847ef917924559 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 19:24:27 -0500 Subject: [PATCH 31/77] Support compiling to an anonymous class. --- src/ProviderCompiler.php | 62 ++++++++++++++++++++++++++ tests/CompiledListenerProviderTest.php | 54 ++++++++++++++++++++++ tests/MakeCompiledProviderTrait.php | 25 +++++++++++ 3 files changed, 141 insertions(+) diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index d0c4f79..67aeaa9 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -8,10 +8,14 @@ use Crell\Tukio\Entry\ListenerFunctionEntry; use Crell\Tukio\Entry\ListenerServiceEntry; use Crell\Tukio\Entry\ListenerStaticMethodEntry; +use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\ListenerProviderInterface; class ProviderCompiler { /** + * Compiles a provided ProviderBuilder to a named class on disk. + * * @param ProviderBuilder $listeners * The set of listeners to compile. * @param resource $stream @@ -36,6 +40,36 @@ public function compile( fwrite($stream, $this->createClosing()); } + /** + * Compiles a provided ProviderBuilder to an anonymous class on disk. + * + * The generated class requires a container instance in its constructor, which + * because it's anonymous has a pre-defined name of $container. That variable must + * be in scope when the resulting file is require()ed/include()ed. The easiest way + * to do that is to use the loadAnonymous() method of this class, but you may also + * do so manually. + * + * @param ProviderBuilder $listeners + * The set of listeners to compile. + * @param resource $stream + * A writeable stream to which to write the compiled class. + */ + public function compileAnonymous(ProviderBuilder $listeners, $stream): void + { + fwrite($stream, $this->createAnonymousPreamble()); + + $this->writeMainListenersList($listeners, $stream); + + $this->writeOptimizedList($listeners, $stream); + + fwrite($stream, $this->createAnonymousClosing()); + } + + public function loadAnonymous(string $filename, ContainerInterface $container): ListenerProviderInterface + { + return require($filename); + } + /** * @param resource $stream * A writeable stream to which to write the compiled code. @@ -163,6 +197,25 @@ public function __construct(ContainerInterface \$container) END; } + protected function createAnonymousPreamble(): string + { + return <<assertEquals('BACD', implode($event->result())); } + + public function test_anonymous_class_compile(): void + { + $builder = new ProviderBuilder(); + + $container = new MockContainer(); + $container->addService('D', new ListenService()); + + $builder->addListener('\\Crell\\Tukio\\listenerA'); + $builder->addListener('\\Crell\\Tukio\\listenerB'); + $builder->addListener('\\Crell\\Tukio\\noListen'); + $builder->addListener([Listen::class, 'listen']); + $builder->addListenerService('D', 'listen', CollectingEvent::class); + + $provider = $this->makeAnonymousProvider($builder, $container); + + $event = new CollectingEvent(); + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $result = $event->result(); + $this->assertContains('A', $result); + $this->assertContains('B', $result); + $this->assertContains('C', $result); + $this->assertContains('D', $result); + + $this->assertTrue(true); + } + + public function test_optimize_event_anonymous_class(): void + { + $builder = new ProviderBuilder(); + $container = new MockContainer(); + + // Just to make the following lines shorter and easier to read. + $ns = '\\Crell\\Tukio\\'; + + $builder->addListener("{$ns}event_listener_one", -4, 'id-1'); + $builder->addListenerBefore('id-1', "{$ns}event_listener_two", 'id-2'); + $builder->addListenerAfter('id-2', "{$ns}event_listener_three", 'id-3'); + $builder->addListenerAfter('id-3', "{$ns}event_listener_four"); + + $builder->optimizeEvents(CollectingEvent::class); + + $provider = $this->makeAnonymousProvider($builder, $container); + + $event = new CollectingEvent(); + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $this->assertEquals('BACD', implode($event->result())); + } } diff --git a/tests/MakeCompiledProviderTrait.php b/tests/MakeCompiledProviderTrait.php index ae352f9..de6030a 100644 --- a/tests/MakeCompiledProviderTrait.php +++ b/tests/MakeCompiledProviderTrait.php @@ -57,4 +57,29 @@ protected function makeProvider(ProviderBuilder $builder, ContainerInterface $co return $provider; } + protected function makeAnonymousProvider(ProviderBuilder $builder, ContainerInterface $container): ListenerProviderInterface + { + try { + $compiler = new ProviderCompiler(); + + // Write the generated compiler out to a temp file. + $filename = tempnam(sys_get_temp_dir(), 'compiled'); + $out = fopen($filename, 'w'); + $compiler->compileAnonymous($builder, $out); + fclose($out); + + // Now include it. If there's a parse error PHP will throw a ParseError and PHPUnit will catch it for us. + $provider = $compiler->loadAnonymous($filename, $container); + } + finally { + // This check is not actually needed as no exception could be + // thrown before $filename gets defined, but PHPStan doesn't + // understand that. + if (isset($filename)) { + unlink($filename); + } + } + + return $provider; + } } From 74b8173f16f7450c96f22e178afc3a4df31ff61f Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 19:52:00 -0500 Subject: [PATCH 32/77] PHPStan level 7. --- phpstan.neon | 8 +----- src/CallbackProvider.php | 2 +- src/CompiledListenerProviderBase.php | 2 +- src/ListenerProxy.php | 11 ++++++++ src/OrderedProviderInterface.php | 2 +- src/ProviderCollector.php | 8 +++++- src/ProviderCompiler.php | 4 ++- tests/MakeCompiledProviderTrait.php | 40 +++++++++++++++------------- 8 files changed, 47 insertions(+), 30 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index aefdbdd..6b721db 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,15 +1,9 @@ parameters: - level: 6 + level: 7 paths: - src - tests checkGenericClassInNonGenericObjectType: false - excludePaths: - # Attribute classes use PHP 8 syntax that's invalid on 7.4, but we don't care. - - src/Listener.php - - src/ListenerBefore.php - - src/ListenerAfter.php - - src/ListenerPriority.php ignoreErrors: - message: '#type has no value type specified in iterable type array#' diff --git a/src/CallbackProvider.php b/src/CallbackProvider.php index 4f03261..b10559b 100644 --- a/src/CallbackProvider.php +++ b/src/CallbackProvider.php @@ -26,7 +26,7 @@ public function getListenersForEvent(object $event): iterable if ($event instanceof $type) { foreach ($callbacks as $callback) { if (method_exists($subject, $callback)) { - yield [$subject, $callback]; + yield $subject->$callback(...); } } } diff --git a/src/CompiledListenerProviderBase.php b/src/CompiledListenerProviderBase.php index 4c37ee1..a26836e 100644 --- a/src/CompiledListenerProviderBase.php +++ b/src/CompiledListenerProviderBase.php @@ -26,7 +26,7 @@ class CompiledListenerProviderBase implements ListenerProviderInterface * The keys are an event class name. The value is an array of callables that may be * returned to the dispatcher as-is. * - * @var array + * @var array> */ protected array $optimized = []; diff --git a/src/ListenerProxy.php b/src/ListenerProxy.php index e562274..07f6fd8 100644 --- a/src/ListenerProxy.php +++ b/src/ListenerProxy.php @@ -14,6 +14,9 @@ class ListenerProxy protected string $serviceName; + /** + * @var class-string + */ protected string $serviceClass; /** @@ -22,6 +25,11 @@ class ListenerProxy */ protected array $registeredMethods = []; + /** + * @param OrderedProviderInterface $provider + * @param string $serviceName + * @param class-string $serviceClass + */ public function __construct(OrderedProviderInterface $provider, string $serviceName, string $serviceClass) { $this->provider = $provider; @@ -124,6 +132,9 @@ public function getRegisteredMethods(): array protected function getServiceMethodType(string $methodName): string { try { + // We don't have a real object here, so we cannot use first-class-closures. + // PHPStan complains that an aray is not a callable, even though it is, because PHP. + // @phpstan-ignore-next-line $type = $this->getParameterType([$this->serviceClass, $methodName]); } catch (\InvalidArgumentException $exception) { throw InvalidTypeException::fromClassCallable($this->serviceClass, $methodName, $exception); diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 03609a6..bbee723 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -191,7 +191,7 @@ public function addListenerServiceAfter(string $after, string $service, string $ * * The event type the listener is for will be derived from the type declaration in the method signature. * - * @param string $class + * @param class-string $class * The class name to be registered as a subscriber. * @param string $service * The name of a service in the container. diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 0c7fcd3..7ed685f 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -125,12 +125,15 @@ protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array return $attributes; } + /** + * @param class-string $class + */ protected function addSubscribersByProxy(string $class, string $service): ListenerProxy { $proxy = new ListenerProxy($this, $service, $class); // Explicit registration is opt-in. - if (in_array(SubscriberInterface::class, class_implements($class), true)) { + if (in_array(SubscriberInterface::class, class_implements($class) ?: [], true)) { /** @var SubscriberInterface $class */ $class::registerListeners($proxy); } @@ -145,6 +148,7 @@ protected function getAttributes(callable $listener): array $ref = null; if ($this->isFunctionCallable($listener)) { + /** @var string $listener */ $ref = new \ReflectionFunction($listener); } elseif ($this->isClassCallable($listener)) { // PHPStan says you cannot use array destructuring on a callable, but you can @@ -186,6 +190,7 @@ protected function getType(callable $listener): string $type = $this->getParameterType($listener); } catch (\InvalidArgumentException $exception) { if ($this->isClassCallable($listener) || $this->isObjectCallable($listener)) { + /** @var array{0: class-string, 1: string} $listener */ throw InvalidTypeException::fromClassCallable($listener[0], $listener[1], $exception); } if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { @@ -217,6 +222,7 @@ protected function getListenerId(callable $listener): ?string return (string)$listener; } if ($this->isClassCallable($listener)) { + /** @var array{0: class-string, 1: string} $listener */ return $listener[0] . '::' . $listener[1]; } if (is_array($listener) && is_object($listener[0])) { diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 67aeaa9..98f613d 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -220,12 +220,14 @@ public function __construct(ContainerInterface \$container) /** * Returns a list of all class and interface parents of a class. * + * @param class-string $class * @return array */ protected function classAncestors(string $class, bool $includeClass = true): array { // These methods both return associative arrays, making + safe. - $ancestors = class_parents($class) + class_implements($class); + /** @var array $ancestors */ + $ancestors = (class_parents($class) ?: []) + (class_implements($class) ?: []); return $includeClass ? [$class => $class] + $ancestors : $ancestors diff --git a/tests/MakeCompiledProviderTrait.php b/tests/MakeCompiledProviderTrait.php index de6030a..44f8bdc 100644 --- a/tests/MakeCompiledProviderTrait.php +++ b/tests/MakeCompiledProviderTrait.php @@ -29,12 +29,19 @@ trait MakeCompiledProviderTrait */ protected function makeProvider(ProviderBuilder $builder, ContainerInterface $container, string $class, string $namespace) : ListenerProviderInterface { + // Write the generated compiler out to a temp file. + $filename = tempnam(sys_get_temp_dir(), 'compiled'); + if (!$filename) { + throw new \RuntimeException('Unable to create temp file for compiled provider.'); + } + $out = fopen($filename, 'w'); + if (!$out) { + throw new \RuntimeException('Unable to open file to write compiled provider.'); + } + try { $compiler = new ProviderCompiler(); - // Write the generated compiler out to a temp file. - $filename = tempnam(sys_get_temp_dir(), 'compiled'); - $out = fopen($filename, 'w'); $compiler->compile($builder, $out, $class, $namespace); fclose($out); @@ -46,12 +53,7 @@ protected function makeProvider(ProviderBuilder $builder, ContainerInterface $co $provider = new $compiledClassName($container); } finally { - // This check is not actually needed as no exception could be - // thrown before $filename gets defined, but PHPStan doesn't - // understand that. - if (isset($filename)) { - unlink($filename); - } + unlink($filename); } return $provider; @@ -59,12 +61,19 @@ protected function makeProvider(ProviderBuilder $builder, ContainerInterface $co protected function makeAnonymousProvider(ProviderBuilder $builder, ContainerInterface $container): ListenerProviderInterface { + // Write the generated compiler out to a temp file. + $filename = tempnam(sys_get_temp_dir(), 'compiled'); + if (!$filename) { + throw new \RuntimeException('Unable to create temp file for compiled provider.'); + } + $out = fopen($filename, 'w'); + if (!$out) { + throw new \RuntimeException('Unable to open file to write compiled provider.'); + } + try { $compiler = new ProviderCompiler(); - // Write the generated compiler out to a temp file. - $filename = tempnam(sys_get_temp_dir(), 'compiled'); - $out = fopen($filename, 'w'); $compiler->compileAnonymous($builder, $out); fclose($out); @@ -72,12 +81,7 @@ protected function makeAnonymousProvider(ProviderBuilder $builder, ContainerInte $provider = $compiler->loadAnonymous($filename, $container); } finally { - // This check is not actually needed as no exception could be - // thrown before $filename gets defined, but PHPStan doesn't - // understand that. - if (isset($filename)) { - unlink($filename); - } + unlink($filename); } return $provider; From 1eee2ad865bec7526a9c5ddf8d08222828f29dae Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 19:53:01 -0500 Subject: [PATCH 33/77] Moar PHP 8 syntax. --- src/ListenerProxy.php | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ListenerProxy.php b/src/ListenerProxy.php index 07f6fd8..8541aea 100644 --- a/src/ListenerProxy.php +++ b/src/ListenerProxy.php @@ -10,15 +10,6 @@ class ListenerProxy { use ParameterDeriverTrait; - protected OrderedProviderInterface $provider; - - protected string $serviceName; - - /** - * @var class-string - */ - protected string $serviceClass; - /** * @var array * Methods that have already been registered on this subscriber, so we know not to double-subscribe them. @@ -30,12 +21,11 @@ class ListenerProxy * @param string $serviceName * @param class-string $serviceClass */ - public function __construct(OrderedProviderInterface $provider, string $serviceName, string $serviceClass) - { - $this->provider = $provider; - $this->serviceName = $serviceName; - $this->serviceClass = $serviceClass; - } + public function __construct( + protected OrderedProviderInterface $provider, + protected string $serviceName, + protected string $serviceClass + ) {} /** * Adds a method on a service as a listener. From e22cba6f433698ee2c7700a781b021ca61e51431 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 19:56:45 -0500 Subject: [PATCH 34/77] PHPStan Level 8. --- phpstan.neon | 2 +- src/ListenerPriority.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 6b721db..2fcd6d2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - src - tests diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index 11e5050..c4e051d 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -9,7 +9,7 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class ListenerPriority extends Listener { - public function __construct(?int $priority, ?string $id = null, ?string $type = null) { + public function __construct(int $priority, ?string $id = null, ?string $type = null) { parent::__construct(id: $id, order: Order::Priority($priority), type: $type); } } From 9f6f8cc3be9494e1dafaacef6003918005e06ae4 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 20:11:08 -0500 Subject: [PATCH 35/77] Remove Interface suffix from internal interface. --- ...tryInterface.php => CompileableListenerEntry.php} | 2 +- src/Entry/ListenerFunctionEntry.php | 2 +- src/Entry/ListenerServiceEntry.php | 2 +- src/Entry/ListenerStaticMethodEntry.php | 2 +- src/ProviderCompiler.php | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) rename src/Entry/{CompileableListenerEntryInterface.php => CompileableListenerEntry.php} (85%) diff --git a/src/Entry/CompileableListenerEntryInterface.php b/src/Entry/CompileableListenerEntry.php similarity index 85% rename from src/Entry/CompileableListenerEntryInterface.php rename to src/Entry/CompileableListenerEntry.php index cd5fdf8..f15de40 100644 --- a/src/Entry/CompileableListenerEntryInterface.php +++ b/src/Entry/CompileableListenerEntry.php @@ -4,7 +4,7 @@ namespace Crell\Tukio\Entry; -interface CompileableListenerEntryInterface +interface CompileableListenerEntry { /** * Extracts relevant information for the listener. diff --git a/src/Entry/ListenerFunctionEntry.php b/src/Entry/ListenerFunctionEntry.php index 690f741..bd32a62 100644 --- a/src/Entry/ListenerFunctionEntry.php +++ b/src/Entry/ListenerFunctionEntry.php @@ -9,7 +9,7 @@ * * @internal */ -class ListenerFunctionEntry extends ListenerEntry implements CompileableListenerEntryInterface +class ListenerFunctionEntry extends ListenerEntry implements CompileableListenerEntry { /** * @return array{ diff --git a/src/Entry/ListenerServiceEntry.php b/src/Entry/ListenerServiceEntry.php index d188086..7d1a8d6 100644 --- a/src/Entry/ListenerServiceEntry.php +++ b/src/Entry/ListenerServiceEntry.php @@ -9,7 +9,7 @@ * * @internal */ -class ListenerServiceEntry implements CompileableListenerEntryInterface +class ListenerServiceEntry implements CompileableListenerEntry { public function __construct( public string $serviceName, diff --git a/src/Entry/ListenerStaticMethodEntry.php b/src/Entry/ListenerStaticMethodEntry.php index 1374f3d..a4712da 100644 --- a/src/Entry/ListenerStaticMethodEntry.php +++ b/src/Entry/ListenerStaticMethodEntry.php @@ -9,7 +9,7 @@ * * @internal */ -class ListenerStaticMethodEntry extends ListenerEntry implements CompileableListenerEntryInterface +class ListenerStaticMethodEntry extends ListenerEntry implements CompileableListenerEntry { public function __construct( public string $class, diff --git a/src/ProviderCompiler.php b/src/ProviderCompiler.php index 98f613d..f35b6fe 100644 --- a/src/ProviderCompiler.php +++ b/src/ProviderCompiler.php @@ -4,7 +4,7 @@ namespace Crell\Tukio; -use Crell\Tukio\Entry\CompileableListenerEntryInterface; +use Crell\Tukio\Entry\CompileableListenerEntry; use Crell\Tukio\Entry\ListenerFunctionEntry; use Crell\Tukio\Entry\ListenerServiceEntry; use Crell\Tukio\Entry\ListenerStaticMethodEntry; @@ -78,7 +78,7 @@ protected function writeMainListenersList(ProviderBuilder $listeners, $stream): { fwrite($stream, $this->startMainListenersList()); - /** @var CompileableListenerEntryInterface $listenerEntry */ + /** @var CompileableListenerEntry $listenerEntry */ foreach ($listeners as $listenerEntry) { $item = $this->createEntry($listenerEntry); fwrite($stream, $item); @@ -103,11 +103,11 @@ protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void fwrite($stream, $this->startOptimizedEntry($event)); $relevantListeners = array_filter($listenerDefs, - static fn(CompileableListenerEntryInterface $entry) + static fn(CompileableListenerEntry $entry) => in_array($entry->getProperties()['type'], $ancestors, true) ); - /** @var CompileableListenerEntryInterface $listenerEntry */ + /** @var CompileableListenerEntry $listenerEntry */ foreach ($relevantListeners as $listenerEntry) { $item = $this->createOptimizedEntry($listenerEntry); fwrite($stream, $item); @@ -133,7 +133,7 @@ protected function endOptimizedEntry(): string END; } - protected function createOptimizedEntry(CompileableListenerEntryInterface $listenerEntry): string + protected function createOptimizedEntry(CompileableListenerEntry $listenerEntry): string { $listener = $listenerEntry->getProperties(); $ret = match ($listener['entryType']) { @@ -148,7 +148,7 @@ protected function createOptimizedEntry(CompileableListenerEntryInterface $liste return $ret . ',' . PHP_EOL; } - protected function createEntry(CompileableListenerEntryInterface $listenerEntry): string + protected function createEntry(CompileableListenerEntry $listenerEntry): string { $listener = $listenerEntry->getProperties(); switch ($listener['entryType']) { From e76d53c05d99e3d5d255b56962417edc6e27dda8 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 1 Oct 2023 20:13:04 -0500 Subject: [PATCH 36/77] More PHP 8 syntax. --- src/DebugEventDispatcher.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/DebugEventDispatcher.php b/src/DebugEventDispatcher.php index 2be17c9..e140769 100644 --- a/src/DebugEventDispatcher.php +++ b/src/DebugEventDispatcher.php @@ -9,10 +9,6 @@ class DebugEventDispatcher implements EventDispatcherInterface { - protected EventDispatcherInterface $dispatcher; - - protected LoggerInterface $logger; - /** * DebugEventDispatcher constructor. * @@ -21,11 +17,10 @@ class DebugEventDispatcher implements EventDispatcherInterface * @param LoggerInterface $logger * The logger service through which to log. */ - public function __construct(EventDispatcherInterface $dispatcher, LoggerInterface $logger) - { - $this->dispatcher = $dispatcher; - $this->logger = $logger; - } + public function __construct( + protected EventDispatcherInterface $dispatcher, + protected LoggerInterface $logger, + ) {} /** * {@inheritdoc} From f801ada995e3f73482b0cb7414f6d5f99ce1f978 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 13 Nov 2023 18:09:36 -0600 Subject: [PATCH 37/77] Merge QA GHA files. --- .github/workflows/phpstan.yaml | 25 --------------- .github/workflows/quality-assurance.yaml | 41 ++++++++++++++++++++++++ .github/workflows/testing.yaml | 26 --------------- 3 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/phpstan.yaml create mode 100644 .github/workflows/quality-assurance.yaml delete mode 100644 .github/workflows/testing.yaml diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml deleted file mode 100644 index a6debc3..0000000 --- a/.github/workflows/phpstan.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: PHPStan checks -on: - push: - branches: - - master - pull_request: ~ - -jobs: - phpstan: - name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }} - runs-on: ubuntu-latest - strategy: - matrix: - php: [ '8.1', '8.2', '8.3' ] - composer-flags: [ '' ] - steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: pcov - tools: composer:v2 - - run: composer update --no-progress ${{ matrix.composer-flags }} - - run: vendor/bin/phpstan diff --git a/.github/workflows/quality-assurance.yaml b/.github/workflows/quality-assurance.yaml new file mode 100644 index 0000000..2213908 --- /dev/null +++ b/.github/workflows/quality-assurance.yaml @@ -0,0 +1,41 @@ +--- +name: Quality assurance +on: + push: + branches: ['master'] + pull_request: ~ + +jobs: + phpunit: + name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }} + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + composer-flags: [ '' ] + phpunit-flags: [ '--coverage-text' ] + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + tools: composer:v2 + - run: composer install --no-progress ${{ matrix.composer-flags }} + - run: vendor/bin/phpunit ${{ matrix.phpunit-flags }} + phpstan: + name: PHPStan checks on ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + composer-flags: [ '' ] + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + tools: composer:v2 + - run: composer install --no-progress ${{ matrix.composer-flags }} + - run: vendor/bin/phpstan diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml deleted file mode 100644 index 5b76332..0000000 --- a/.github/workflows/testing.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: PHPUnit tests -on: - push: - branches: - - master - pull_request: ~ - -jobs: - phpunit: - name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }} - runs-on: ubuntu-latest - strategy: - matrix: - php: [ '8.1', '8.2', '8.3' ] - composer-flags: [ '' ] - phpunit-flags: [ '--coverage-text' ] - steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: pcov - tools: composer:v2 - - run: composer update --no-progress ${{ matrix.composer-flags }} - - run: vendor/bin/phpunit ${{ matrix.phpunit-flags }} From 4e6a22169f55324595be066dc9fb1332101eed46 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 13 Nov 2023 18:16:41 -0600 Subject: [PATCH 38/77] Switch to PHPUnit attributes. --- tests/CallbackProviderTest.php | 7 ++++-- .../CompiledListenerProviderAttributeTest.php | 7 ++++-- ...ompiledListenerProviderInheritanceTest.php | 10 ++++++--- tests/CompiledListenerProviderTest.php | 22 +++++++++++++------ tests/DebugEventDispatcherTest.php | 4 +++- tests/DispatcherTest.php | 13 +++++++---- ...edListenerProviderAttributeServiceTest.php | 4 +++- .../OrderedListenerProviderAttributeTest.php | 19 +++++++++++----- tests/OrderedListenerProviderIdTest.php | 16 +++++++++----- tests/OrderedListenerProviderServiceTest.php | 7 ++++-- tests/OrderedListenerProviderTest.php | 22 +++++++++++++------ 11 files changed, 91 insertions(+), 40 deletions(-) diff --git a/tests/CallbackProviderTest.php b/tests/CallbackProviderTest.php index 2dec7da..c6e004f 100644 --- a/tests/CallbackProviderTest.php +++ b/tests/CallbackProviderTest.php @@ -5,6 +5,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -57,7 +58,8 @@ public function all(LifecycleEvent $event): void class CallbackProviderTest extends TestCase { - public function test_callback(): void + #[Test] + public function callback_provider(): void { $p = new CallbackProvider(); @@ -76,7 +78,8 @@ public function test_callback(): void $this->assertEquals('AD', implode($event->result())); } - public function test_non_callback_event_skips_silently(): void + #[Test] + public function non_callback_event_skips_silently(): void { $p = new CallbackProvider(); diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 0671d42..3dd0709 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[ListenerPriority(3, 'A', CollectingEvent::class)] @@ -48,7 +49,8 @@ class CompiledListenerProviderAttributeTest extends TestCase { use MakeCompiledProviderTrait; - function test_compiled_provider_triggers_in_order(): void + #[Test] + public function compiled_provider_triggers_in_order(): void { $class = 'AtCompiledProvider'; $namespace = 'Test\\Space'; @@ -75,7 +77,8 @@ function test_compiled_provider_triggers_in_order(): void $this->assertEquals('ABC', implode($event->result())); } - public function test_add_subscriber(): void + #[Test] + public function add_subscriber(): void { // This test is parallel to and uses the same mock subscriber as // RegisterableListenerProviderServiceTest::test_add_subscriber(). diff --git a/tests/CompiledListenerProviderInheritanceTest.php b/tests/CompiledListenerProviderInheritanceTest.php index 971a18b..ad1c28e 100644 --- a/tests/CompiledListenerProviderInheritanceTest.php +++ b/tests/CompiledListenerProviderInheritanceTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; interface EventParentInterface @@ -62,7 +63,8 @@ class CompiledListenerProviderInheritanceTest extends TestCase { use MakeCompiledProviderTrait; - public function test_interface_listener_catches_everything(): void + #[Test] + public function interface_listener_catches_everything(): void { $class = __FUNCTION__; $namespace = 'Test\\Space'; @@ -91,7 +93,8 @@ public function test_interface_listener_catches_everything(): void } } - public function test_class_listener_catches_subclass(): void + #[Test] + public function class_listener_catches_subclass(): void { $class = __FUNCTION__; $namespace = 'Test\\Space'; @@ -120,7 +123,8 @@ public function test_class_listener_catches_subclass(): void } } - public function test_subclass_listener_catches_subclass(): void + #[Test] + public function subclass_listener_catches_subclass(): void { $class = __FUNCTION__; $namespace = 'Test\\Space'; diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 05e54a0..9896702 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; function listenerA(CollectingEvent $event): void @@ -44,7 +45,8 @@ class CompiledListenerProviderTest extends TestCase { use MakeCompiledProviderTrait; - function test_compiled_provider_triggers_in_order(): void + #[Test] + public function compiled_provider_triggers_in_order(): void { $class = 'CompiledProvider'; $namespace = 'Test\\Space'; @@ -76,7 +78,8 @@ function test_compiled_provider_triggers_in_order(): void $this->assertTrue(true); } - public function test_add_subscriber(): void + #[Test] + public function add_subscriber(): void { // This test is parallel to and uses the same mock subscriber as // OrderedListenerProviderServiceTest::test_add_subscriber(). @@ -103,7 +106,8 @@ public function test_add_subscriber(): void $this->assertEquals('BCAEDF', implode($event->result())); } - public function test_natural_id_on_compiled_provider(): void + #[Test] + public function natural_id_on_compiled_provider(): void { $class = 'NaturalIdProvider'; $namespace = 'Test\\Space'; @@ -129,7 +133,8 @@ public function test_natural_id_on_compiled_provider(): void $this->assertEquals('BACD', implode($event->result())); } - public function test_explicit_id_on_compiled_provider(): void + #[Test] + public function explicit_id_on_compiled_provider(): void { $class = 'ExplicitIdProvider'; $namespace = 'Test\\Space'; @@ -155,7 +160,8 @@ public function test_explicit_id_on_compiled_provider(): void $this->assertEquals('BACD', implode($event->result())); } - public function test_optimize_event(): void + #[Test] + public function optimize_event(): void { $class = 'OptimizedEventProvider'; $namespace = 'Test\\Space'; @@ -183,7 +189,8 @@ public function test_optimize_event(): void $this->assertEquals('BACD', implode($event->result())); } - public function test_anonymous_class_compile(): void + #[Test] + public function anonymous_class_compile(): void { $builder = new ProviderBuilder(); @@ -212,7 +219,8 @@ public function test_anonymous_class_compile(): void $this->assertTrue(true); } - public function test_optimize_event_anonymous_class(): void + #[Test] + public function optimize_event_anonymous_class(): void { $builder = new ProviderBuilder(); $container = new MockContainer(); diff --git a/tests/DebugEventDispatcherTest.php b/tests/DebugEventDispatcherTest.php index 5023c76..638b28b 100644 --- a/tests/DebugEventDispatcherTest.php +++ b/tests/DebugEventDispatcherTest.php @@ -5,6 +5,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\AbstractLogger; @@ -22,7 +23,8 @@ public function setUp(): void $this->logger = new MockLogger(); } - public function test_event_is_logged() : void + #[Test] + public function event_is_logged() : void { $inner = new class implements EventDispatcherInterface { public function dispatch(object $event) diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 1509162..6275f78 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Log\AbstractLogger; @@ -21,7 +22,8 @@ public function setUp(): void $this->logger = new MockLogger(); } - public function test_dispatcher_calls_all_listeners() : void + #[Test] + public function dispatcher_calls_all_listeners() : void { $provider = new class implements ListenerProviderInterface { public function getListenersForEvent(object $event): iterable @@ -42,7 +44,8 @@ public function getListenersForEvent(object $event): iterable $this->assertEquals('CRELL', implode($event->result())); } - public function test_stoppable_events_stop() : void { + #[Test] + public function stoppable_events_stop() : void { $provider = new class implements ListenerProviderInterface { public function getListenersForEvent(object $event): iterable { @@ -62,7 +65,8 @@ public function getListenersForEvent(object $event): iterable $this->assertEquals('CRE', implode($event->result())); } - public function test_listener_exception_logged() : void + #[Test] + public function listener_exception_logged() : void { $provider = new class implements ListenerProviderInterface { public function getListenersForEvent(object $event): iterable @@ -95,7 +99,8 @@ public function getListenersForEvent(object $event): iterable $this->assertEquals($event, $entry['context']['event']); } - public function test_already_stopped_event_calls_no_listeners() : void + #[Test] + public function already_stopped_event_calls_no_listeners() : void { $provider = new class implements ListenerProviderInterface { public function getListenersForEvent(object $event): iterable diff --git a/tests/OrderedListenerProviderAttributeServiceTest.php b/tests/OrderedListenerProviderAttributeServiceTest.php index 9b7c034..daedb6a 100644 --- a/tests/OrderedListenerProviderAttributeServiceTest.php +++ b/tests/OrderedListenerProviderAttributeServiceTest.php @@ -5,11 +5,13 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; class OrderedListenerProviderAttributeServiceTest extends TestCase { - public function test_add_subscriber() : void + #[Test] + public function add_subscriber() : void { $container = new MockContainer(); diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index 5915d4c..a04d7c0 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[ListenerPriority(0, 'a')] @@ -75,7 +76,8 @@ function at_multi_one(CollectingEvent $event): void class OrderedListenerProviderAttributeTest extends TestCase { - public function test_id_from_attribute_is_found() : void + #[Test] + public function id_from_attribute_is_found() : void { $p = new OrderedListenerProvider(); @@ -95,7 +97,8 @@ public function test_id_from_attribute_is_found() : void $this->assertEquals('BA', implode($event->result())); } - public function test_priority_from_attribute_honored() : void + #[Test] + public function priority_from_attribute_honored() : void { $p = new OrderedListenerProvider(); @@ -114,7 +117,8 @@ public function test_priority_from_attribute_honored() : void $this->assertEquals('CA', implode($event->result())); } - public function test_type_from_attribute_called_correctly() : void + #[Test] + public function type_from_attribute_called_correctly() : void { $p = new OrderedListenerProvider(); @@ -134,7 +138,8 @@ public function test_type_from_attribute_called_correctly() : void $this->assertEquals('CDA', implode($event->result())); } - public function test_type_from_attribute_skips_correctly() : void + #[Test] + public function type_from_attribute_skips_correctly() : void { $p = new OrderedListenerProvider(); @@ -154,7 +159,8 @@ public function test_type_from_attribute_skips_correctly() : void $this->assertEquals(false, $event->called); } - public function test_attributes_found_on_object_methods() : void + #[Test] + public function attributes_found_on_object_methods() : void { $p = new OrderedListenerProvider(); @@ -172,7 +178,8 @@ public function test_attributes_found_on_object_methods() : void $this->assertEquals('DC', implode($event->result())); } - public function test_before_after_methods_win_over_attributes(): void + #[Test] + public function before_after_methods_win_over_attributes(): void { $p = new OrderedListenerProvider(); diff --git a/tests/OrderedListenerProviderIdTest.php b/tests/OrderedListenerProviderIdTest.php index 6fda91d..c4102b1 100644 --- a/tests/OrderedListenerProviderIdTest.php +++ b/tests/OrderedListenerProviderIdTest.php @@ -5,6 +5,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; function event_listener_one(CollectingEvent $event): void @@ -53,7 +54,8 @@ public function listenerD(CollectingEvent $event): void class OrderedListenerProviderIdTest extends TestCase { - public function test_natural_id_for_function(): void + #[Test] + public function natural_id_for_function(): void { $p = new OrderedListenerProvider(); @@ -74,7 +76,8 @@ public function test_natural_id_for_function(): void $this->assertEquals('BACD', implode($event->result())); } - public function test_natural_id_for_static_method(): void + #[Test] + public function natural_id_for_static_method(): void { $p = new OrderedListenerProvider(); @@ -90,7 +93,8 @@ public function test_natural_id_for_static_method(): void $this->assertEquals('BA', implode($event->result())); } - public function test_natural_id_for_object_method(): void + #[Test] + public function natural_id_for_object_method(): void { $p = new OrderedListenerProvider(); @@ -108,7 +112,8 @@ public function test_natural_id_for_object_method(): void $this->assertEquals('DC', implode($event->result())); } - public function test_explicit_id_for_function(): void + #[Test] + public function explicit_id_for_function(): void { $p = new OrderedListenerProvider(); @@ -129,7 +134,8 @@ public function test_explicit_id_for_function(): void $this->assertEquals('BACD', implode($event->result())); } - public function test_natural_id_for_service_listener(): void + #[Test] + public function natural_id_for_service_listener(): void { $container = new MockContainer(); diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 101231b..505e056 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -5,6 +5,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; class OrderedListenerProviderServiceTest extends TestCase @@ -64,7 +65,8 @@ public function hear(CollectingEvent $event): void $this->mockContainer = $container; } - public function test_add_listener_service(): void + #[Test] + public function add_listener_service(): void { $p = new OrderedListenerProvider($this->mockContainer); @@ -83,7 +85,8 @@ public function test_add_listener_service(): void $this->assertEquals('CRELL', implode($event->result())); } - public function test_add_listener_service_before_another(): void + #[Test] + public function add_listener_service_before_another(): void { $p = new OrderedListenerProvider($this->mockContainer); diff --git a/tests/OrderedListenerProviderTest.php b/tests/OrderedListenerProviderTest.php index 04e835f..972454e 100644 --- a/tests/OrderedListenerProviderTest.php +++ b/tests/OrderedListenerProviderTest.php @@ -5,6 +5,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; class EventOne extends CollectingEvent {} @@ -13,7 +14,8 @@ class EventTwo extends CollectingEvent {} class OrderedListenerProviderTest extends TestCase { - public function test_only_type_correct_listeners_are_returned(): void + #[Test] + public function only_type_correct_listeners_are_returned(): void { $p = new OrderedListenerProvider(); @@ -42,7 +44,8 @@ public function test_only_type_correct_listeners_are_returned(): void $this->assertEquals('YY', implode($event->result())); } - public function test_add_ordered_listeners(): void + #[Test] + public function add_ordered_listeners(): void { $p = new OrderedListenerProvider(); @@ -71,7 +74,8 @@ public function test_add_ordered_listeners(): void $this->assertEquals('CRELL', implode($event->result())); } - public function test_add_listener_before(): void + #[Test] + public function add_listener_before(): void { $p = new OrderedListenerProvider(); @@ -100,7 +104,8 @@ public function test_add_listener_before(): void $this->assertEquals('CRELL', implode($event->result())); } - public function test_add_listener_after(): void + #[Test] + public function add_listener_after(): void { $p = new OrderedListenerProvider(); @@ -129,7 +134,8 @@ public function test_add_listener_after(): void $this->assertEquals('CRELL', implode($event->result())); } - public function test_add_malformed_listener(): void + #[Test] + public function add_malformed_listener(): void { $this->expectException(InvalidTypeException::class); @@ -140,7 +146,8 @@ public function test_add_malformed_listener(): void }); } - public function test_add_malformed_listener_before(): void + #[Test] + public function add_malformed_listener_before(): void { $this->expectException(InvalidTypeException::class); @@ -154,7 +161,8 @@ public function test_add_malformed_listener_before(): void }); } - public function test_add_malformed_listener_after(): void + #[Test] + public function add_malformed_listener_after(): void { $this->expectException(InvalidTypeException::class); From 8ac82992a94bd9062d2afd111b0f317813b59330 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 21 Nov 2023 18:50:33 -0600 Subject: [PATCH 39/77] Switch to OrderedCollection v2 and use the new multi-ordering collection. --- CHANGELOG.md | 20 +++++++++++++++++++ composer.json | 2 +- src/OrderedListenerProvider.php | 6 ------ src/ProviderCollector.php | 10 +++++----- .../CompiledListenerProviderAttributeTest.php | 8 +++++++- tests/CompiledListenerProviderTest.php | 8 +++++++- ...edListenerProviderAttributeServiceTest.php | 8 +++++++- tests/OrderedListenerProviderServiceTest.php | 8 +++++++- 8 files changed, 54 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e69172..a55f7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to `Tukio` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 2.0.0 - YYYY-MM-DD + +### Added +- Major internal refactoring. +- There is now an `add()` method on the Provider and Compiler classes that allows specifying multiple before/after rules at once, in addition to priority. It is *recommended* to use this method in place of the older ones. +- Upgraded to OrderedCollection v2, and switched to a Topological-based sort. The main advantage is the ability to support multiple before/after rules. However, this has a side effect that the order of listeners that had no relative order specified may have changed. This is not an API break as that order was never guaranteed, but may still affect some order-sensitive code that worked by accident. If that happens, and you care about the order, specify before/after orders as appropriate. + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing + + ## 1.5.0 - 2023-03-25 ### Added diff --git a/composer.json b/composer.json index cb92af9..28969d5 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": "~8.1", - "crell/ordered-collection": "^1.0", + "crell/ordered-collection": "v2.x-dev", "fig/event-dispatcher-util": "^1.3", "psr/container": "^1.0 || ^2.0", "psr/event-dispatcher": "^1.0", diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 619db5b..ba2916d 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -4,18 +4,12 @@ namespace Crell\Tukio; -use Crell\OrderedCollection\OrderedCollection; use Crell\Tukio\Entry\ListenerEntry; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\ListenerProviderInterface; class OrderedListenerProvider extends ProviderCollector implements ListenerProviderInterface { - /** - * @var OrderedCollection - */ - protected OrderedCollection $listeners; - public function __construct(protected ?ContainerInterface $container = null) { parent::__construct(); diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 7ed685f..3983b44 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -4,7 +4,7 @@ namespace Crell\Tukio; -use Crell\OrderedCollection\OrderedCollection; +use Crell\OrderedCollection\MultiOrderedCollection; use Crell\Tukio\Entry\ListenerEntry; use Fig\EventDispatcher\ParameterDeriverTrait; @@ -13,13 +13,13 @@ abstract class ProviderCollector implements OrderedProviderInterface use ParameterDeriverTrait; /** - * @var OrderedCollection + * @var MultiOrderedCollection */ - protected OrderedCollection $listeners; + protected MultiOrderedCollection $listeners; public function __construct() { - $this->listeners = new OrderedCollection(); + $this->listeners = new MultiOrderedCollection(); } public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string @@ -77,7 +77,7 @@ public function addSubscriber(string $class, string $service): void $methods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); $methods = array_filter($methods, static fn(\ReflectionMethod $r) - => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); + => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); /** @var \ReflectionMethod $rMethod */ foreach ($methods as $rMethod) { diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 3dd0709..38e9ef6 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -102,6 +102,12 @@ public function add_subscriber(): void $listener($event); } - $this->assertEquals('BCAEDF', implode($event->result())); + // We can't guarantee a stricter order than the instructions provided, so + // just check for those rather than a precise order. + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'C') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'D') > strpos($result, 'A')); + self::assertTrue(strpos($result, 'F') > strpos($result, 'A')); } } diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 9896702..9b0d4ad 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -103,7 +103,13 @@ public function add_subscriber(): void $listener($event); } - $this->assertEquals('BCAEDF', implode($event->result())); + // We can't guarantee a stricter order than the instructions provided, so + // just check for those rather than a precise order. + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'C') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'D') > strpos($result, 'A')); + self::assertTrue(strpos($result, 'F') > strpos($result, 'A')); } #[Test] diff --git a/tests/OrderedListenerProviderAttributeServiceTest.php b/tests/OrderedListenerProviderAttributeServiceTest.php index daedb6a..06f5d2c 100644 --- a/tests/OrderedListenerProviderAttributeServiceTest.php +++ b/tests/OrderedListenerProviderAttributeServiceTest.php @@ -29,6 +29,12 @@ public function add_subscriber() : void $listener($event); } - $this->assertEquals('BCAEDF', implode($event->result())); + // We can't guarantee a stricter order than the instructions provided, so + // just check for those rather than a precise order. + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'C') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'D') > strpos($result, 'A')); + self::assertTrue(strpos($result, 'F') > strpos($result, 'A')); } } diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 505e056..54085d3 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -152,7 +152,13 @@ public function test_add_subscriber() : void $listener($event); } - $this->assertEquals('BCAEDF', implode($event->result())); + // We can't guarantee a stricter order than the instructions provided, so + // just check for those rather than a precise order. + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'C') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'D') > strpos($result, 'A')); + self::assertTrue(strpos($result, 'F') > strpos($result, 'A')); } public function test_malformed_subscriber_automatic_fails(): void From f2c2e44f567d7d1aa83b8675be82e93dd62a6eb6 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 21 Nov 2023 19:03:46 -0600 Subject: [PATCH 40/77] Use proper PHPUnit assertion syntax. --- tests/CallbackProviderTest.php | 4 +-- .../CompiledListenerProviderAttributeTest.php | 2 +- ...ompiledListenerProviderInheritanceTest.php | 6 ++-- tests/CompiledListenerProviderTest.php | 28 +++++++++---------- tests/DebugEventDispatcherTest.php | 6 ++-- tests/DispatcherTest.php | 18 ++++++------ .../OrderedListenerProviderAttributeTest.php | 14 +++++----- tests/OrderedListenerProviderIdTest.php | 12 ++++---- tests/OrderedListenerProviderServiceTest.php | 6 ++-- tests/OrderedListenerProviderTest.php | 8 +++--- 10 files changed, 52 insertions(+), 52 deletions(-) diff --git a/tests/CallbackProviderTest.php b/tests/CallbackProviderTest.php index c6e004f..eb3f4bb 100644 --- a/tests/CallbackProviderTest.php +++ b/tests/CallbackProviderTest.php @@ -75,7 +75,7 @@ public function callback_provider(): void $listener($event); } - $this->assertEquals('AD', implode($event->result())); + self::assertEquals('AD', implode($event->result())); } #[Test] @@ -93,6 +93,6 @@ public function non_callback_event_skips_silently(): void $listener($event); } - $this->assertEquals('', implode($event->result())); + self::assertEquals('', implode($event->result())); } } diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 38e9ef6..7c84e9d 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -74,7 +74,7 @@ public function compiled_provider_triggers_in_order(): void $listener($event); } - $this->assertEquals('ABC', implode($event->result())); + self::assertEquals('ABC', implode($event->result())); } #[Test] diff --git a/tests/CompiledListenerProviderInheritanceTest.php b/tests/CompiledListenerProviderInheritanceTest.php index ad1c28e..36fd9cf 100644 --- a/tests/CompiledListenerProviderInheritanceTest.php +++ b/tests/CompiledListenerProviderInheritanceTest.php @@ -89,7 +89,7 @@ public function interface_listener_catches_everything(): void foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); } - $this->assertEquals($result, implode($event->result())); + self::assertEquals($result, implode($event->result())); } } @@ -119,7 +119,7 @@ public function class_listener_catches_subclass(): void foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); } - $this->assertEquals($result, implode($event->result())); + self::assertEquals($result, implode($event->result())); } } @@ -149,7 +149,7 @@ public function subclass_listener_catches_subclass(): void foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); } - $this->assertEquals($result, implode($event->result())); + self::assertEquals($result, implode($event->result())); } } diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 9b0d4ad..4e127a7 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -70,12 +70,12 @@ public function compiled_provider_triggers_in_order(): void } $result = $event->result(); - $this->assertContains('A', $result); - $this->assertContains('B', $result); - $this->assertContains('C', $result); - $this->assertContains('D', $result); + self::assertContains('A', $result); + self::assertContains('B', $result); + self::assertContains('C', $result); + self::assertContains('D', $result); - $this->assertTrue(true); + self::assertTrue(true); } #[Test] @@ -136,7 +136,7 @@ public function natural_id_on_compiled_provider(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } #[Test] @@ -163,7 +163,7 @@ public function explicit_id_on_compiled_provider(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } #[Test] @@ -192,7 +192,7 @@ public function optimize_event(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } #[Test] @@ -217,12 +217,12 @@ public function anonymous_class_compile(): void } $result = $event->result(); - $this->assertContains('A', $result); - $this->assertContains('B', $result); - $this->assertContains('C', $result); - $this->assertContains('D', $result); + self::assertContains('A', $result); + self::assertContains('B', $result); + self::assertContains('C', $result); + self::assertContains('D', $result); - $this->assertTrue(true); + self::assertTrue(true); } #[Test] @@ -248,6 +248,6 @@ public function optimize_event_anonymous_class(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } } diff --git a/tests/DebugEventDispatcherTest.php b/tests/DebugEventDispatcherTest.php index 638b28b..a2d7a61 100644 --- a/tests/DebugEventDispatcherTest.php +++ b/tests/DebugEventDispatcherTest.php @@ -38,8 +38,8 @@ public function dispatch(object $event) $event = new CollectingEvent(); $p->dispatch($event); - $this->assertCount(1, $this->logger->messages[LogLevel::DEBUG]); - $this->assertEquals('Processing event of type {type}.', $this->logger->messages[LogLevel::DEBUG][0]['message']); - $this->assertEquals($event, $this->logger->messages[LogLevel::DEBUG][0]['context']['event']); + self::assertCount(1, $this->logger->messages[LogLevel::DEBUG]); + self::assertEquals('Processing event of type {type}.', $this->logger->messages[LogLevel::DEBUG][0]['message']); + self::assertEquals($event, $this->logger->messages[LogLevel::DEBUG][0]['context']['event']); } } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 6275f78..c1648c3 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -41,7 +41,7 @@ public function getListenersForEvent(object $event): iterable $event = new CollectingEvent(); $p->dispatch($event); - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } #[Test] @@ -62,7 +62,7 @@ public function getListenersForEvent(object $event): iterable $event = new StoppableCollectingEvent(); $p->dispatch($event); - $this->assertEquals('CRE', implode($event->result())); + self::assertEquals('CRE', implode($event->result())); } #[Test] @@ -87,16 +87,16 @@ public function getListenersForEvent(object $event): iterable $this->fail('No exception was bubbled up.'); } catch (\Exception $e) { - $this->assertEquals('Fail!', $e->getMessage()); + self::assertEquals('Fail!', $e->getMessage()); } - $this->assertEquals('CR', implode($event->result())); + self::assertEquals('CR', implode($event->result())); - $this->assertArrayHasKey(LogLevel::WARNING, $this->logger->messages); - $this->assertCount(1, $this->logger->messages[LogLevel::WARNING]); + self::assertArrayHasKey(LogLevel::WARNING, $this->logger->messages); + self::assertCount(1, $this->logger->messages[LogLevel::WARNING]); $entry = $this->logger->messages[LogLevel::WARNING][0]; - $this->assertEquals('Unhandled exception thrown from listener while processing event.', $entry['message']); - $this->assertEquals($event, $entry['context']['event']); + self::assertEquals('Unhandled exception thrown from listener while processing event.', $entry['message']); + self::assertEquals($event, $entry['context']['event']); } #[Test] @@ -116,6 +116,6 @@ public function getListenersForEvent(object $event): iterable $d->dispatch($event); - $this->assertEquals('', implode($event->result())); + self::assertEquals('', implode($event->result())); } } diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index a04d7c0..0ea6a39 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -93,8 +93,8 @@ public function id_from_attribute_is_found() : void $listener($event); } - $this->assertEquals('a', $id_one); - $this->assertEquals('BA', implode($event->result())); + self::assertEquals('a', $id_one); + self::assertEquals('BA', implode($event->result())); } #[Test] @@ -114,7 +114,7 @@ public function priority_from_attribute_honored() : void $listener($event); } - $this->assertEquals('CA', implode($event->result())); + self::assertEquals('CA', implode($event->result())); } #[Test] @@ -135,7 +135,7 @@ public function type_from_attribute_called_correctly() : void $listener($event); } - $this->assertEquals('CDA', implode($event->result())); + self::assertEquals('CDA', implode($event->result())); } #[Test] @@ -156,7 +156,7 @@ public function type_from_attribute_skips_correctly() : void $listener($event); } - $this->assertEquals(false, $event->called); + self::assertEquals(false, $event->called); } #[Test] @@ -175,7 +175,7 @@ public function attributes_found_on_object_methods() : void $listener($event); } - $this->assertEquals('DC', implode($event->result())); + self::assertEquals('DC', implode($event->result())); } #[Test] @@ -196,6 +196,6 @@ public function before_after_methods_win_over_attributes(): void $listener($event); } - $this->assertEquals('CAD', implode($event->result())); + self::assertEquals('CAD', implode($event->result())); } } diff --git a/tests/OrderedListenerProviderIdTest.php b/tests/OrderedListenerProviderIdTest.php index c4102b1..15a06d4 100644 --- a/tests/OrderedListenerProviderIdTest.php +++ b/tests/OrderedListenerProviderIdTest.php @@ -73,7 +73,7 @@ public function natural_id_for_function(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } #[Test] @@ -90,7 +90,7 @@ public function natural_id_for_static_method(): void $listener($event); } - $this->assertEquals('BA', implode($event->result())); + self::assertEquals('BA', implode($event->result())); } #[Test] @@ -109,7 +109,7 @@ public function natural_id_for_object_method(): void $listener($event); } - $this->assertEquals('DC', implode($event->result())); + self::assertEquals('DC', implode($event->result())); } #[Test] @@ -131,7 +131,7 @@ public function explicit_id_for_function(): void $listener($event); } - $this->assertEquals('BACD', implode($event->result())); + self::assertEquals('BACD', implode($event->result())); } #[Test] @@ -164,8 +164,8 @@ public function listen(CollectingEvent $event): void $listener($event); } - $this->assertEquals('A-listen', $idA); - $this->assertEquals('AB', implode($event->result())); + self::assertEquals('A-listen', $idA); + self::assertEquals('AB', implode($event->result())); } } diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 54085d3..9f1385d 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -82,7 +82,7 @@ public function add_listener_service(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } #[Test] @@ -102,7 +102,7 @@ public function add_listener_service_before_another(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } public function test_add_listener_service_after_another(): void @@ -121,7 +121,7 @@ public function test_add_listener_service_after_another(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } public function test_service_registration_fails_without_container(): void diff --git a/tests/OrderedListenerProviderTest.php b/tests/OrderedListenerProviderTest.php index 972454e..db57828 100644 --- a/tests/OrderedListenerProviderTest.php +++ b/tests/OrderedListenerProviderTest.php @@ -41,7 +41,7 @@ public function only_type_correct_listeners_are_returned(): void $listener($event); } - $this->assertEquals('YY', implode($event->result())); + self::assertEquals('YY', implode($event->result())); } #[Test] @@ -71,7 +71,7 @@ public function add_ordered_listeners(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } #[Test] @@ -101,7 +101,7 @@ public function add_listener_before(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } #[Test] @@ -131,7 +131,7 @@ public function add_listener_after(): void $listener($event); } - $this->assertEquals('CRELL', implode($event->result())); + self::assertEquals('CRELL', implode($event->result())); } #[Test] From 81270a20cdf749b7b9a5b84210256bedd02c90a2 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 28 Nov 2023 20:22:10 -0600 Subject: [PATCH 41/77] Correct changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a55f7c6..24772cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Major internal refactoring. -- There is now an `add()` method on the Provider and Compiler classes that allows specifying multiple before/after rules at once, in addition to priority. It is *recommended* to use this method in place of the older ones. +- There is now a `listener()` method on the Provider and Compiler classes that allows specifying multiple before/after rules at once, in addition to priority. It is *recommended* to use this method in place of the older ones. - Upgraded to OrderedCollection v2, and switched to a Topological-based sort. The main advantage is the ability to support multiple before/after rules. However, this has a side effect that the order of listeners that had no relative order specified may have changed. This is not an API break as that order was never guaranteed, but may still affect some order-sensitive code that worked by accident. If that happens, and you care about the order, specify before/after orders as appropriate. ### Deprecated From b864c385e87db8d3b7738e1445c7382e49acfa46 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 28 Nov 2023 20:22:51 -0600 Subject: [PATCH 42/77] Support multiple before/after directives, part 1. --- src/Listener.php | 50 +++++++++++++-- src/ListenerAfter.php | 12 +++- src/ListenerBefore.php | 12 +++- src/ListenerPriority.php | 14 ++-- src/ProviderCollector.php | 64 +++++++++++++------ .../CompiledListenerProviderAttributeTest.php | 3 +- ...eredListenerProviderMultiAttributeTest.php | 63 ++++++++++++++++++ tests/OrderedListenerProviderServiceTest.php | 2 +- tests/OrderedListenerProviderTest.php | 32 ++++++---- 9 files changed, 205 insertions(+), 47 deletions(-) create mode 100644 tests/OrderedListenerProviderMultiAttributeTest.php diff --git a/src/Listener.php b/src/Listener.php index 9851577..5bd3506 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -9,12 +9,14 @@ /** * The main attribute to customize a listener. */ -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class Listener implements ListenerAttribute { + public array $before = []; + public array $after = []; + public ?int $priority = null; + /** - * @param Order|null $order - * One of Order::Priority(), Order::Before(), or Order::After(). * @param ?string $id * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type @@ -23,7 +25,47 @@ class Listener implements ListenerAttribute */ public function __construct( public ?string $id = null, - public ?Order $order = null, public ?string $type = null, ) {} + + /** + * @param array $attribs + */ + public function absorbBefore(array $attribs): void + { + foreach ($attribs as $attrib) { + $this->id ??= $attrib->id; + $this->type ??= $attrib->type; + $this->before = [...$this->before, $attrib->order->before]; + } + } + + /** + * @param array $attribs + */ + public function absorbAfter(array $attribs): void + { + foreach ($attribs as $attrib) { + $this->id ??= $attrib->id; + $this->type ??= $attrib->type; + $this->after = [...$this->after, $attrib->order->after]; + } + } + + public function absorbPriority(ListenerPriority $attrib): void + { + $this->id ??= $attrib->id; + $this->type ??= $attrib->type; + $this->priority = $attrib->order->priority; + } + + public function absorbOrder(Order $order): void + { + match (true) { + $order instanceof OrderBefore => $this->before = [...$this->before, $order->before], + $order instanceof OrderAfter => $this->after = [...$this->after, $order->after], + $order instanceof OrderPriority => $this->priority = $order->priority, + default => null, + }; + } } diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index 4fefed4..04dca35 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -7,9 +7,15 @@ use Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerAfter extends Listener +class ListenerAfter implements ListenerAttribute { - public function __construct(string $after, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, order: Order::After($after), type: $type); + public ?Order $order = null; + + public function __construct( + string $after, + public ?string $id = null, + public ?string $type = null, + ) { + $this->order = Order::After($after); } } diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index d5e9e99..21f497b 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -7,9 +7,15 @@ use Attribute; #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerBefore extends Listener +class ListenerBefore implements ListenerAttribute { - public function __construct(string $before, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, order: Order::Before($before), type: $type); + public ?Order $order = null; + + public function __construct( + string $before, + public ?string $id = null, + public ?string $type = null, + ) { + $this->order = Order::Before($before); } } diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index c4e051d..dd324a6 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -6,10 +6,16 @@ use Attribute; -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerPriority extends Listener +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] +class ListenerPriority implements ListenerAttribute { - public function __construct(int $priority, ?string $id = null, ?string $type = null) { - parent::__construct(id: $id, order: Order::Priority($priority), type: $type); + public ?Order $order = null; + + public function __construct( + int $priority, + public ?string $id = null, + public ?string $type = null, + ) { + $this->order = Order::Priority($priority); } } diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 3983b44..de22237 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -24,24 +24,29 @@ public function __construct() public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string { - $attrib = $this->getAttributes($listener)[0] ?? null; - $id ??= $order->id ?? $attrib?->id ?? $this->getListenerId($listener); - $type ??= $order->type ?? $attrib?->type ?? $this->getType($listener); - $order ??= $attrib?->order; + /** @var Listener $def */ + $def = $this->getAttributeDefinition($listener); + $id ??= $order->id ?? $def?->id ?? $this->getListenerId($listener); + $type ??= $type ?? $def?->type ?? $this->getType($listener); + + if ($order) { + $def->absorbOrder($order); + } $entry = $this->getListenerEntry($listener, $type); - return match (true) { - $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), - $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), - $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), - default => $this->listeners->addItem($entry, id: $id), - }; + return $this->listeners->add( + item: $entry, + id: $id, + priority: $def->priority, + before: $def->before, + after: $def->after + ); } public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { - return $this->listener($listener, $priority ? Order::Priority($priority) : null, $id, $type); + return $this->listener($listener, is_null($priority) ? null : Order::Priority($priority), $id, $type); } public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string @@ -110,7 +115,7 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class // @phpstan-ignore-next-line $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); - $this->listenerService($service, $methodName, $type, $attrib?->order, $id); + $this->listenerService($service, $methodName, $type, null, $id); } } @@ -140,10 +145,7 @@ protected function addSubscribersByProxy(string $class, string $service): Listen return $proxy; } - /** - * @return array - */ - protected function getAttributes(callable $listener): array + protected function getAttributeDefinition(callable $listener): Listener { $ref = null; @@ -165,11 +167,37 @@ protected function getAttributes(callable $listener): array } if (!$ref) { - return []; + return new Listener(); + } + + // All this logic is very similar to AttributeUtils Sub-Attributes. + // Maybe AU can be improved to make sub-attributes accessible outside + // the analyzer? + + $def = $this->getAttributes(Listener::class, $ref)[0] ?? new Listener(); + + $beforeAttribs = $this->getAttributes(ListenerBefore::class, $ref); + $def->absorbBefore($beforeAttribs); + + $afterAttribs = $this->getAttributes(ListenerAfter::class, $ref); + $def->absorbAfter($afterAttribs); + + $priorityAttribs = $this->getAttributes(ListenerPriority::class, $ref)[0] ?? null; + if ($priorityAttribs) { + $def->absorbPriority($priorityAttribs); } - $attribs = $ref->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF); + return $def; + } + /** + * @param class-string $attribute + * @param \Reflector $ref + * @return array + */ + protected function getAttributes(string $attribute, \Reflector $ref): array + { + $attribs = $ref->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF); return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); } diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 7c84e9d..41b93dc 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -74,7 +74,8 @@ public function compiled_provider_triggers_in_order(): void $listener($event); } - self::assertEquals('ABC', implode($event->result())); + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') > strpos($result, 'A')); } #[Test] diff --git a/tests/OrderedListenerProviderMultiAttributeTest.php b/tests/OrderedListenerProviderMultiAttributeTest.php new file mode 100644 index 0000000..102352f --- /dev/null +++ b/tests/OrderedListenerProviderMultiAttributeTest.php @@ -0,0 +1,63 @@ +add('A'); +} + +#[Listener('b')] +function listener_b(CollectingEvent $event): void +{ + $event->add('B'); +} + +function listener_c(CollectingEvent $event): void +{ + $event->add('C'); +} + +#[ListenerBefore('listener_a')] +#[ListenerBefore('b')] +function listener_d(CollectingEvent $event): void +{ + $event->add('D'); +} + +class OrderedListenerProviderMultiAttributeTest extends TestCase +{ + #[Test] + public function ordering_with_multiple_before_after_rules_works(): void + { + $p = new OrderedListenerProvider(); + + // Just to make the following lines shorter and easier to read. + $ns = '\\Crell\\Tukio\\'; + + $p->listener("{$ns}listener_a"); + $p->listener("{$ns}listener_b"); + $p->listener("{$ns}listener_c"); + $p->listener("{$ns}listener_d"); + + $event = new CollectingEvent(); + + foreach ($p->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $result = implode($event->result()); + self::assertTrue(strpos($result, 'A') > strpos($result, 'B')); + self::assertTrue(strpos($result, 'A') > strpos($result, 'C')); + self::assertTrue(strpos($result, 'D') < strpos($result, 'A')); + self::assertTrue(strpos($result, 'D') < strpos($result, 'B')); + } + +} diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 9f1385d..9c353b5 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -73,7 +73,7 @@ public function add_listener_service(): void $p->addListenerService('L', 'hear', CollectingEvent::class, 70); $p->addListenerService('E', 'listen', CollectingEvent::class, 80); $p->addListenerService('C', 'listen', CollectingEvent::class, 100); - $p->addListenerService('L', 'hear', CollectingEvent::class); // Defaults to 0 + $p->addListenerService('L', 'hear', CollectingEvent::class, priority: 0); $p->addListenerService('R', 'listen', CollectingEvent::class, 90); $event = new CollectingEvent(); diff --git a/tests/OrderedListenerProviderTest.php b/tests/OrderedListenerProviderTest.php index db57828..d607d30 100644 --- a/tests/OrderedListenerProviderTest.php +++ b/tests/OrderedListenerProviderTest.php @@ -80,20 +80,20 @@ public function add_listener_before(): void $p = new OrderedListenerProvider(); $p->addListener(function (CollectingEvent $event) { - $event->add('E'); - }, 0); - $rid = $p->addListener(function (CollectingEvent $event) { - $event->add('R'); - }, 90); + $event->add('A'); + }, 0, id: 'A'); + $bid = $p->addListener(function (CollectingEvent $event) { + $event->add('B'); + }, 90, id: 'B'); $p->addListener(function (CollectingEvent $event) { - $event->add('L'); - }, 0); - $p->addListenerBefore($rid, function (CollectingEvent $event) { $event->add('C'); - }); + }, -5, id: 'C'); + $p->addListenerBefore($bid, function (CollectingEvent $event) { + $event->add('D'); + }, id: 'D'); $p->addListener(function (CollectingEvent $event) { - $event->add('L'); - }, 0); + $event->add('E'); + }, 0, id: 'E'); $event = new CollectingEvent(); @@ -101,7 +101,13 @@ public function add_listener_before(): void $listener($event); } - self::assertEquals('CRELL', implode($event->result())); + $result = implode($event->result()); + self::assertTrue(strpos($result, 'B') < strpos($result, 'C')); + self::assertTrue(strpos($result, 'D') < strpos($result, 'B')); + self::assertTrue(strpos($result, 'B') < strpos($result, 'E')); + self::assertTrue(strpos($result, 'E') < strpos($result, 'C')); + +// self::assertEquals('CRELL', implode($event->result())); } #[Test] @@ -114,7 +120,7 @@ public function add_listener_after(): void }, 90); $p->addListener(function (CollectingEvent $event) { $event->add('L'); - }, 0); + }, -5); $p->addListenerBefore($rid, function (CollectingEvent $event) { $event->add('C'); }); From f1a3ff461dea12a535777080677a43f76c7e8d6e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 1 Dec 2023 18:12:49 -0600 Subject: [PATCH 43/77] Remove the Order pseudo-enum, as before/after/priority are no longer exclusive. Finish the multi-entry support. --- src/Listener.php | 16 +---- src/ListenerAfter.php | 6 +- src/ListenerBefore.php | 6 +- src/ListenerPriority.php | 8 +-- src/Order.php | 63 ++++++++++--------- src/OrderedListenerProvider.php | 12 +++- src/OrderedProviderInterface.php | 38 ++++++++--- src/ProviderBuilder.php | 17 ++--- src/ProviderCollector.php | 48 +++++++++----- ...eredListenerProviderMultiAttributeTest.php | 4 +- tests/OrderedListenerProviderTest.php | 22 +++---- 11 files changed, 133 insertions(+), 107 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index 5bd3506..3724f17 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -36,7 +36,7 @@ public function absorbBefore(array $attribs): void foreach ($attribs as $attrib) { $this->id ??= $attrib->id; $this->type ??= $attrib->type; - $this->before = [...$this->before, $attrib->order->before]; + $this->before = [...$this->before, ...$attrib->before]; } } @@ -48,7 +48,7 @@ public function absorbAfter(array $attribs): void foreach ($attribs as $attrib) { $this->id ??= $attrib->id; $this->type ??= $attrib->type; - $this->after = [...$this->after, $attrib->order->after]; + $this->after = [...$this->after, ...$attrib->after]; } } @@ -56,16 +56,6 @@ public function absorbPriority(ListenerPriority $attrib): void { $this->id ??= $attrib->id; $this->type ??= $attrib->type; - $this->priority = $attrib->order->priority; - } - - public function absorbOrder(Order $order): void - { - match (true) { - $order instanceof OrderBefore => $this->before = [...$this->before, $order->before], - $order instanceof OrderAfter => $this->after = [...$this->after, $order->after], - $order instanceof OrderPriority => $this->priority = $order->priority, - default => null, - }; + $this->priority = $attrib->priority; } } diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index 04dca35..c3577a9 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -9,13 +9,13 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class ListenerAfter implements ListenerAttribute { - public ?Order $order = null; + public array $after = []; public function __construct( - string $after, + string|array $after, public ?string $id = null, public ?string $type = null, ) { - $this->order = Order::After($after); + $this->after = is_array($after) ? $after : [$after]; } } diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index 21f497b..6ba52a7 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -9,13 +9,13 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class ListenerBefore implements ListenerAttribute { - public ?Order $order = null; + public array $before = []; public function __construct( - string $before, + string|array $before, public ?string $id = null, public ?string $type = null, ) { - $this->order = Order::Before($before); + $this->before = is_array($before) ? $before : [$before]; } } diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index dd324a6..6dd8612 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -9,13 +9,9 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] class ListenerPriority implements ListenerAttribute { - public ?Order $order = null; - public function __construct( - int $priority, + public int $priority, public ?string $id = null, public ?string $type = null, - ) { - $this->order = Order::Priority($priority); - } + ) {} } diff --git a/src/Order.php b/src/Order.php index 13d74fe..c168013 100644 --- a/src/Order.php +++ b/src/Order.php @@ -7,35 +7,36 @@ /** * This class fantasizes of being an ADT Enum. Convert it as soon as possible. */ -abstract class Order -{ - public static function Priority(int $priority): OrderPriority - { - return new OrderPriority(priority: $priority); - } - public static function Before(string $before): OrderBefore - { - return new OrderBefore(before: $before); - } - - public static function After(string $after): OrderAfter - { - return new OrderAfter(after: $after); - } -} - -final class OrderPriority extends Order -{ - public function __construct(public readonly int $priority) {} -} - -final class OrderBefore extends Order -{ - public function __construct(public readonly string $before) {} -} - -final class OrderAfter extends Order -{ - public function __construct(public readonly string $after) {} -} +//abstract class Order +//{ +// public static function Priority(int $priority): OrderPriority +// { +// return new OrderPriority(priority: $priority); +// } +// +// public static function Before(string $before): OrderBefore +// { +// return new OrderBefore(before: $before); +// } +// +// public static function After(string $after): OrderAfter +// { +// return new OrderAfter(after: $after); +// } +//} +// +//final class OrderPriority extends Order +//{ +// public function __construct(public readonly int $priority) {} +//} +// +//final class OrderBefore extends Order +//{ +// public function __construct(public readonly string $before) {} +//} +// +//final class OrderAfter extends Order +//{ +// public function __construct(public readonly string $after) {} +//} diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index ba2916d..6f7008d 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -33,10 +33,18 @@ protected function getListenerEntry(callable $listener, string $type): ListenerE return new ListenerEntry($listener, $type); } - public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string + public function listenerService( + string $service, + string $method, + string $type, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null + ): string { $id ??= $service . '-' . $method; - return $this->listener($this->makeListenerForService($service, $method), $order, $id, $type); + return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } /** diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index bbee723..149aa51 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -11,9 +11,13 @@ interface OrderedProviderInterface * Adds a listener to the provider. * * @param callable $listener - * The listener to register. - * @param Order|null $order - * One of Order::Priority(), Order::Before(), or Order::After(). + * The listener to register. + * @param int|null $priority + * The numeric priority of the listener. + * @param array $before + * A list of listener IDs this listener should come before. + * @param array $after + * A list of listener IDs this listener should come after. * @param ?string $id * The identifier by which this listener should be known. If not specified one will be generated. * @param ?string $type @@ -22,7 +26,14 @@ interface OrderedProviderInterface * * @return string * The opaque ID of the listener. This can be used for future reference. */ - public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string; + public function listener( + callable $listener, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null, + ?string $type = null + ): string; /** * Adds a method on a service as a listener. @@ -35,13 +46,24 @@ public function listener(callable $listener, ?Order $order = null, ?string $id = * The method name of the service that is the listener being registered. * @param string $type * The class or interface type of events for which this listener will be registered. - * @param Order|null $order - * One of Order::Priority(), Order::Before(), or Order::After(). - * + * @param int|null $priority + * The numeric priority of the listener. + * @param array $before + * A list of listener IDs this listener should come before. + * @param array $after + * A list of listener IDs this listener should come after. * @return string * The opaque ID of the listener. This can be used for future reference. */ - public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string; + public function listenerService( + string $service, + string $method, + string $type, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null + ): string; /** * Adds a listener to the provider. diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 23f091e..f4f82ad 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -34,17 +34,20 @@ public function getOptimizedEvents(): array return $this->optimizedEvents; } - public function listenerService(string $service, string $method, string $type, ?Order $order = null, ?string $id = null): string + public function listenerService( + string $service, + string $method, + string $type, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null + ): string { $entry = new ListenerServiceEntry($service, $method, $type); $id ??= $service . '-' . $method; - return match (true) { - $order instanceof OrderBefore => $this->listeners->addItemBefore($order->before, $entry, $id), - $order instanceof OrderAfter => $this->listeners->addItemAfter($order->after, $entry, $id), - $order instanceof OrderPriority => $this->listeners->addItem($entry, $order->priority, $id), - default => $this->listeners->addItem($entry, id: $id), - }; + return $this->listeners->add($entry, $id, priority: $priority, before: $before, after: $after); } public function getIterator(): \Traversable diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index de22237..34daeb1 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -22,15 +22,26 @@ public function __construct() $this->listeners = new MultiOrderedCollection(); } - public function listener(callable $listener, ?Order $order = null, ?string $id = null, ?string $type = null): string + public function listener( + callable $listener, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null, + ?string $type = null + ): string { /** @var Listener $def */ $def = $this->getAttributeDefinition($listener); - $id ??= $order->id ?? $def?->id ?? $this->getListenerId($listener); + $id ??= $def?->id ?? $this->getListenerId($listener); $type ??= $type ?? $def?->type ?? $this->getType($listener); - if ($order) { - $def->absorbOrder($order); + // If any ordering is specified explicitly, that completely overrules any + // attributes. + if (!is_null($priority) || $before || $after) { + $def->priority = $priority; + $def->before = $before; + $def->after = $after; } $entry = $this->getListenerEntry($listener, $type); @@ -46,32 +57,32 @@ public function listener(callable $listener, ?Order $order = null, ?string $id = public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string { - return $this->listener($listener, is_null($priority) ? null : Order::Priority($priority), $id, $type); + return $this->listener($listener, priority: $priority, id: $id, type: $type); } public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string { - return $this->listener($listener, $before ? Order::Before($before) : null, $id, $type); + return $this->listener($listener, before: [$before], id: $id, type: $type); } public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string { - return $this->listener($listener, $after ? Order::After($after) : null, $id, $type); + return $this->listener($listener, after: [$after], id: $id, type: $type); } public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string { - return $this->listenerService($service, $method, $type, ($priority !== null) ? Order::Priority($priority) : null, $id); + return $this->listenerService($service, $method, $type, priority: $priority, id: $id); } public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string { - return $this->listenerService($service, $method, $type, $before ? Order::Before($before) : null, $id); + return $this->listenerService($service, $method, $type, before: [$before], id: $id); } public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string { - return $this->listenerService($service, $method, $type, $after ? Order::After($after) : null, $id); + return $this->listenerService($service, $method, $type, after: [$after], id: $id); } public function addSubscriber(string $class, string $service): void @@ -103,19 +114,17 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class return; } - $attributes = $this->findAttributesOnMethod($rMethod); - /** @var ?Listener $attrib */ - $attrib = $attributes[0] ?? null; + $def = $this->getAttributeForRef($rMethod); - if (str_starts_with($methodName, 'on') || $attrib) { + if (str_starts_with($methodName, 'on') || $def) { $paramType = $params[0]->getType(); - $id = $attrib->id ?? $service . '-' . $methodName; + $id = $def->id ?? $service . '-' . $methodName; // getName() is not a documented part of the Reflection API, but it's always there. // @phpstan-ignore-next-line - $type = $attrib->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); + $type = $def->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); - $this->listenerService($service, $methodName, $type, null, $id); + $this->listenerService($service, $methodName, $type, $def->priority, $def->before,$def->after, $id); } } @@ -170,6 +179,11 @@ protected function getAttributeDefinition(callable $listener): Listener return new Listener(); } + return $this->getAttributeForRef($ref); + } + + protected function getAttributeForRef(\Reflector $ref): Listener + { // All this logic is very similar to AttributeUtils Sub-Attributes. // Maybe AU can be improved to make sub-attributes accessible outside // the analyzer? diff --git a/tests/OrderedListenerProviderMultiAttributeTest.php b/tests/OrderedListenerProviderMultiAttributeTest.php index 102352f..e3ef09e 100644 --- a/tests/OrderedListenerProviderMultiAttributeTest.php +++ b/tests/OrderedListenerProviderMultiAttributeTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; #[ListenerAfter('b')] -#[ListenerAfter('listener_c')] +#[ListenerAfter('\\Crell\\Tukio\\listener_c')] function listener_a(CollectingEvent $event): void { $event->add('A'); @@ -25,7 +25,7 @@ function listener_c(CollectingEvent $event): void $event->add('C'); } -#[ListenerBefore('listener_a')] +#[ListenerBefore('\\Crell\\Tukio\\listener_a')] #[ListenerBefore('b')] function listener_d(CollectingEvent $event): void { diff --git a/tests/OrderedListenerProviderTest.php b/tests/OrderedListenerProviderTest.php index d607d30..eadbec6 100644 --- a/tests/OrderedListenerProviderTest.php +++ b/tests/OrderedListenerProviderTest.php @@ -79,21 +79,11 @@ public function add_listener_before(): void { $p = new OrderedListenerProvider(); - $p->addListener(function (CollectingEvent $event) { - $event->add('A'); - }, 0, id: 'A'); - $bid = $p->addListener(function (CollectingEvent $event) { - $event->add('B'); - }, 90, id: 'B'); - $p->addListener(function (CollectingEvent $event) { - $event->add('C'); - }, -5, id: 'C'); - $p->addListenerBefore($bid, function (CollectingEvent $event) { - $event->add('D'); - }, id: 'D'); - $p->addListener(function (CollectingEvent $event) { - $event->add('E'); - }, 0, id: 'E'); + $p->addListener(fn (CollectingEvent $event) => $event->add('A'), 0, id: 'A'); + $bid = $p->addListener(fn (CollectingEvent $event) => $event->add('B'), 90, id: 'B'); + $p->addListener(fn (CollectingEvent $event) => $event->add('C'), -5, id: 'C'); + $p->addListenerBefore($bid, fn (CollectingEvent $event) => $event->add('D'), id: 'D'); + $p->addListener(fn (CollectingEvent $event) => $event->add('E'), 0, id: 'E'); $event = new CollectingEvent(); @@ -102,6 +92,8 @@ public function add_listener_before(): void } $result = implode($event->result()); + self::assertTrue(strpos($result, 'A') < strpos($result, 'C')); + self::assertTrue(strpos($result, 'A') > strpos($result, 'B')); self::assertTrue(strpos($result, 'B') < strpos($result, 'C')); self::assertTrue(strpos($result, 'D') < strpos($result, 'B')); self::assertTrue(strpos($result, 'B') < strpos($result, 'E')); From 899ccb26e11fa3d9e085e6db97cc944583773222 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 1 Dec 2023 19:34:53 -0600 Subject: [PATCH 44/77] PHPStan fixes. --- src/Listener.php | 3 +++ src/ListenerAfter.php | 4 ++++ src/ListenerBefore.php | 4 ++++ src/ProviderCollector.php | 15 +++++++++++---- tests/MockLogger.php | 3 ++- tests/OrderedListenerProviderAttributeTest.php | 8 -------- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index 3724f17..b5b4384 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -12,7 +12,10 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class Listener implements ListenerAttribute { + /** @var string[] */ public array $before = []; + + /** @var string[] */ public array $after = []; public ?int $priority = null; diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index c3577a9..ef5e1e5 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -9,8 +9,12 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class ListenerAfter implements ListenerAttribute { + /** @var string[] */ public array $after = []; + /** + * @param string|array $after + */ public function __construct( string|array $after, public ?string $id = null, diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index 6ba52a7..db0db92 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -9,8 +9,12 @@ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class ListenerBefore implements ListenerAttribute { + /** @var string[] */ public array $before = []; + /** + * @param string|array $before + */ public function __construct( string|array $before, public ?string $id = null, diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 34daeb1..44428d4 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -34,7 +34,7 @@ public function listener( /** @var Listener $def */ $def = $this->getAttributeDefinition($listener); $id ??= $def?->id ?? $this->getListenerId($listener); - $type ??= $type ?? $def?->type ?? $this->getType($listener); + $type ??= $def?->type ?? $this->getType($listener); // If any ordering is specified explicitly, that completely overrules any // attributes. @@ -188,17 +188,21 @@ protected function getAttributeForRef(\Reflector $ref): Listener // Maybe AU can be improved to make sub-attributes accessible outside // the analyzer? + /** @var Listener $def */ $def = $this->getAttributes(Listener::class, $ref)[0] ?? new Listener(); + /** @var ListenerBefore[] $beforeAttribs */ $beforeAttribs = $this->getAttributes(ListenerBefore::class, $ref); $def->absorbBefore($beforeAttribs); + /** @var ListenerAfter[] $afterAttribs */ $afterAttribs = $this->getAttributes(ListenerAfter::class, $ref); $def->absorbAfter($afterAttribs); - $priorityAttribs = $this->getAttributes(ListenerPriority::class, $ref)[0] ?? null; - if ($priorityAttribs) { - $def->absorbPriority($priorityAttribs); + /** @var ListenerPriority|null $priorityAttrib */ + $priorityAttrib = $this->getAttributes(ListenerPriority::class, $ref)[0] ?? null; + if ($priorityAttrib) { + $def->absorbPriority($priorityAttrib); } return $def; @@ -211,6 +215,9 @@ protected function getAttributeForRef(\Reflector $ref): Listener */ protected function getAttributes(string $attribute, \Reflector $ref): array { + // The Reflector interface doesn't have getAttributes() defined, but + // it's always there. PHP bug. + // @phpstan-ignore-next-line $attribs = $ref->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF); return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); } diff --git a/tests/MockLogger.php b/tests/MockLogger.php index 5d00cf1..ede5387 100644 --- a/tests/MockLogger.php +++ b/tests/MockLogger.php @@ -6,7 +6,8 @@ use Psr\Log\AbstractLogger; -class MockLogger extends AbstractLogger { +class MockLogger extends AbstractLogger +{ /** * @var array diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index 0ea6a39..9b1ad77 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -66,14 +66,6 @@ public function listenerD(CollectingEvent $event) : void } } -#[Listener('A')] -#[Listener('B')] -#[Listener('C')] -function at_multi_one(CollectingEvent $event): void -{ - $event->add('A'); -} - class OrderedListenerProviderAttributeTest extends TestCase { #[Test] From 145bb0d97b7be45dfc3dba6c9a19a1abfdc35e32 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 1 Dec 2023 19:37:39 -0600 Subject: [PATCH 45/77] Bug fix found by PHPStan. --- src/ProviderCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 44428d4..54f3ad6 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -116,7 +116,7 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class $def = $this->getAttributeForRef($rMethod); - if (str_starts_with($methodName, 'on') || $def) { + if ($def->id || $def->before || $def->after || $def->priority || str_starts_with($methodName, 'on')) { $paramType = $params[0]->getType(); $id = $def->id ?? $service . '-' . $methodName; From 11dca375908fc9bca7d68b312b0bdaecf7cb8636 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 4 Dec 2023 18:39:48 -0600 Subject: [PATCH 46/77] Don't QA on unsupported PHP versions. --- .github/workflows/quality-assurance.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/quality-assurance.yaml b/.github/workflows/quality-assurance.yaml index 2213908..dd958f1 100644 --- a/.github/workflows/quality-assurance.yaml +++ b/.github/workflows/quality-assurance.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + php: [ '8.1', '8.2', '8.3' ] composer-flags: [ '' ] phpunit-flags: [ '--coverage-text' ] steps: @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + php: [ '8.1', '8.2', '8.3' ] composer-flags: [ '' ] steps: - uses: actions/checkout@v2 From c5b2133a615b0d5780b492444b29fd6d47e5cb40 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 15:02:28 -0600 Subject: [PATCH 47/77] Add auto-detection of simple service listeners. --- src/OrderedListenerProvider.php | 30 ++++- src/ServiceRegistrationClassNotExists.php | 19 +++ src/ServiceRegistrationTooManyMethods.php | 19 +++ tests/OrderedListenerProviderServiceTest.php | 132 ++++++++++++++++++- 4 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 src/ServiceRegistrationClassNotExists.php create mode 100644 src/ServiceRegistrationTooManyMethods.php diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 6f7008d..e3a6c75 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -35,14 +35,40 @@ protected function getListenerEntry(callable $listener, string $type): ListenerE public function listenerService( string $service, - string $method, - string $type, + ?string $method = null, + ?string $type = null, ?int $priority = null, array $before = [], array $after = [], ?string $id = null ): string { + if (!$method) { + if (!class_exists($service)) { + throw ServiceRegistrationClassNotExists::create($service); + } + $rClass = new \ReflectionClass($service); + $rMethods = $rClass->getMethods(); + // If the class has only one method, assume that's the listener. + // Otherwise, use __invoke if not otherwise specified. + // Otherwise, we cannot tell what to do so throw. + if (count($rMethods) === 1) { + $method = $rMethods[0]->name; + } else if($rClass->hasMethod('__invoke')) { + $method = '__invoke'; + } + else { + throw ServiceRegistrationTooManyMethods::create($service); + } + } + + if (!$type) { + if (!class_exists($service)) { + throw ServiceRegistrationClassNotExists::create($service); + } + $type = $this->getParameterType([$service, $method]); + } + $id ??= $service . '-' . $method; return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } diff --git a/src/ServiceRegistrationClassNotExists.php b/src/ServiceRegistrationClassNotExists.php new file mode 100644 index 0000000..05f641e --- /dev/null +++ b/src/ServiceRegistrationClassNotExists.php @@ -0,0 +1,19 @@ +service = $service; + $msg = 'Tukio can auto-detect the type and method for a listener service only if the service ID is a valid class name. The service "%s" does not exist. Please specify the $method and $type parameters explicitly, or check that you are using the right service name.'; + + $new->message = sprintf($msg, $service); + + return $new; + } +} diff --git a/src/ServiceRegistrationTooManyMethods.php b/src/ServiceRegistrationTooManyMethods.php new file mode 100644 index 0000000..65a8f6d --- /dev/null +++ b/src/ServiceRegistrationTooManyMethods.php @@ -0,0 +1,19 @@ +service = $service; + $msg = 'Tukio can auto-detect a single method on a listener service, or use one named __invoke(). The "%s" service has too many methods not named __invoke(). Please check your class or use a subscriber.'; + + $new->message = sprintf($msg, $service); + + return $new; + } +} diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 9c353b5..5e42185 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -4,14 +4,53 @@ namespace Crell\Tukio; - use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +class InvokableListener +{ + public function __invoke(CollectingEvent $event): void + { + $event->add(static::class); + } +} +class ArbitraryListener +{ + public function doStuff(CollectingEvent $event): void + { + $event->add(static::class); + } +} + +class CompoundListener +{ + public function __invoke(CollectingEvent $event): void + { + $event->add(static::class); + } + + public function dontUseThis(CollectingEvent $event): void + { + throw new \Exception('This should not get called.'); + } +} + +class InvalidListener +{ + public function useThis(CollectingEvent $event): void + { + $event->add(static::class); + } + + public function dontUseThis(CollectingEvent $event): void + { + throw new \Exception('This should not get called.'); + } +} + class OrderedListenerProviderServiceTest extends TestCase { - /** @var MockContainer */ - protected $mockContainer; + protected MockContainer $mockContainer; public function setUp(): void { @@ -226,4 +265,91 @@ public function test_malformed_subscriber_manual_after_fails(): void MockMalformedSubscriber::registerListenersAfter($proxy); } + + #[Test] + public function detects_invoke_method_and_type(): void + { + $container = new MockContainer(); + + $container->addService(InvokableListener::class, new InvokableListener()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(InvokableListener::class); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals(InvokableListener::class, $event->result()[0]); + } + + #[Test] + public function detects_arbitrary_method_and_type(): void + { + $container = new MockContainer(); + + $container->addService(ArbitraryListener::class, new ArbitraryListener()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(ArbitraryListener::class); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals(ArbitraryListener::class, $event->result()[0]); + } + + #[Test] + public function detects_compound_method_and_type(): void + { + $container = new MockContainer(); + + $container->addService(CompoundListener::class, new CompoundListener()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(CompoundListener::class); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals(CompoundListener::class, $event->result()[0]); + } + + #[Test] + public function rejects_multi_method_class_without_invoke(): void + { + $this->expectException(ServiceRegistrationTooManyMethods::class); + $container = new MockContainer(); + + $container->addService(InvalidListener::class, new InvalidListener()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(InvalidListener::class); + } + #[Test] + public function rejects_missing_auto_detected_service(): void + { + $this->expectException(ServiceRegistrationClassNotExists::class); + $container = new MockContainer(); + + $container->addService(InvalidListener::class, new InvalidListener()); + + $provider = new OrderedListenerProvider($container); + + /** @phpsan-ignore-next-line */ + $provider->listenerService(DoesNotExist::class); + } + } From c16ebeed4a5d1ad21b288af3041e4259f1106858 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 15:06:46 -0600 Subject: [PATCH 48/77] Refactor for cleanliness. --- src/OrderedListenerProvider.php | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index e3a6c75..acc2405 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -41,26 +41,8 @@ public function listenerService( array $before = [], array $after = [], ?string $id = null - ): string - { - if (!$method) { - if (!class_exists($service)) { - throw ServiceRegistrationClassNotExists::create($service); - } - $rClass = new \ReflectionClass($service); - $rMethods = $rClass->getMethods(); - // If the class has only one method, assume that's the listener. - // Otherwise, use __invoke if not otherwise specified. - // Otherwise, we cannot tell what to do so throw. - if (count($rMethods) === 1) { - $method = $rMethods[0]->name; - } else if($rClass->hasMethod('__invoke')) { - $method = '__invoke'; - } - else { - throw ServiceRegistrationTooManyMethods::create($service); - } - } + ): string { + $method ??= $this->deriveMethod($service); if (!$type) { if (!class_exists($service)) { @@ -73,6 +55,24 @@ public function listenerService( return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } + protected function deriveMethod(string $service): string + { + if (!class_exists($service)) { + throw ServiceRegistrationClassNotExists::create($service); + } + $rClass = new \ReflectionClass($service); + $rMethods = $rClass->getMethods(); + + // If the class has only one method, assume that's the listener. + // Otherwise, use __invoke if not otherwise specified. + // Otherwise, we cannot tell what to do so throw. + return match (true) { + count($rMethods) === 1 => $rMethods[0]->name, + $rClass->hasMethod('__invoke') => '__invoke', + default => throw ServiceRegistrationTooManyMethods::create($service), + }; + } + /** * Creates a callable that will proxy to the provided service and method. * From f7b551a1a87b7dd6cde44f27a344686dc0fd60a7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 15:08:31 -0600 Subject: [PATCH 49/77] More PHPUnit standardization. --- tests/OrderedListenerProviderServiceTest.php | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 5e42185..08103c7 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -144,7 +144,8 @@ public function add_listener_service_before_another(): void self::assertEquals('CRELL', implode($event->result())); } - public function test_add_listener_service_after_another(): void + #[Test] + public function add_listener_service_after_another(): void { $p = new OrderedListenerProvider($this->mockContainer); @@ -163,7 +164,8 @@ public function test_add_listener_service_after_another(): void self::assertEquals('CRELL', implode($event->result())); } - public function test_service_registration_fails_without_container(): void + #[Test] + public function service_registration_fails_without_container(): void { $this->expectException(ContainerMissingException::class); @@ -173,7 +175,8 @@ public function test_service_registration_fails_without_container(): void } - public function test_add_subscriber() : void + #[Test] + public function add_subscriber() : void { $container = new MockContainer(); @@ -200,7 +203,8 @@ public function test_add_subscriber() : void self::assertTrue(strpos($result, 'F') > strpos($result, 'A')); } - public function test_malformed_subscriber_automatic_fails(): void + #[Test] + public function malformed_subscriber_automatic_fails(): void { $this->expectException(InvalidTypeException::class); @@ -215,7 +219,8 @@ public function test_malformed_subscriber_automatic_fails(): void $p->addSubscriber(MockMalformedSubscriber::class, 'subscriber'); } - public function test_malformed_subscriber_manual_fails(): void + #[Test] + public function malformed_subscriber_manual_fails(): void { $this->expectException(InvalidTypeException::class); @@ -232,7 +237,8 @@ public function test_malformed_subscriber_manual_fails(): void MockMalformedSubscriber::registerListenersDirect($proxy); } - public function test_malformed_subscriber_manual_before_fails(): void + #[Test] + public function malformed_subscriber_manual_before_fails(): void { $this->expectException(InvalidTypeException::class); @@ -249,7 +255,8 @@ public function test_malformed_subscriber_manual_before_fails(): void MockMalformedSubscriber::registerListenersBefore($proxy); } - public function test_malformed_subscriber_manual_after_fails(): void + #[Test] + public function malformed_subscriber_manual_after_fails(): void { $this->expectException(InvalidTypeException::class); From b9292ff51d36a17a288a8c13f68cc767498d9276 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 16:25:06 -0600 Subject: [PATCH 50/77] Add service derivation to the compiled container, too. --- src/OrderedListenerProvider.php | 18 ------ src/OrderedProviderInterface.php | 13 +++-- src/ProviderBuilder.php | 16 ++++-- src/ProviderCollector.php | 21 ++++++- tests/CompiledListenerProviderTest.php | 60 ++++++++++++++++++++ tests/OrderedListenerProviderServiceTest.php | 57 +++++-------------- 6 files changed, 113 insertions(+), 72 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index acc2405..1007663 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -55,24 +55,6 @@ public function listenerService( return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } - protected function deriveMethod(string $service): string - { - if (!class_exists($service)) { - throw ServiceRegistrationClassNotExists::create($service); - } - $rClass = new \ReflectionClass($service); - $rMethods = $rClass->getMethods(); - - // If the class has only one method, assume that's the listener. - // Otherwise, use __invoke if not otherwise specified. - // Otherwise, we cannot tell what to do so throw. - return match (true) { - count($rMethods) === 1 => $rMethods[0]->name, - $rClass->hasMethod('__invoke') => '__invoke', - default => throw ServiceRegistrationTooManyMethods::create($service), - }; - } - /** * Creates a callable that will proxy to the provided service and method. * diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 149aa51..8d71734 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -42,10 +42,15 @@ public function listener( * * @param string $service * The name of a service on which this listener lives. - * @param string $method + * @param string|null $method * The method name of the service that is the listener being registered. - * @param string $type + * If not specified, the collector will attempt to derive it on the + * assumption the class and service name are the same. A single-method + * class will use that single method. Otherwise, __invoke() will be assumed. + * @param string|null $type * The class or interface type of events for which this listener will be registered. + * If not specified, the collector will attempt to derive it on the assumption + * that the class and service name are the same. * @param int|null $priority * The numeric priority of the listener. * @param array $before @@ -57,8 +62,8 @@ public function listener( */ public function listenerService( string $service, - string $method, - string $type, + ?string $method = null, + ?string $type = null, ?int $priority = null, array $before = [], array $after = [], diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index f4f82ad..72ba697 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -36,14 +36,22 @@ public function getOptimizedEvents(): array public function listenerService( string $service, - string $method, - string $type, + ?string $method = null, + ?string $type = null, ?int $priority = null, array $before = [], array $after = [], ?string $id = null - ): string - { + ): string { + $method ??= $this->deriveMethod($service); + + if (!$type) { + if (!class_exists($service)) { + throw ServiceRegistrationClassNotExists::create($service); + } + $type = $this->getParameterType([$service, $method]); + } + $entry = new ListenerServiceEntry($service, $method, $type); $id ??= $service . '-' . $method; diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 54f3ad6..9026fb9 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -29,8 +29,7 @@ public function listener( array $after = [], ?string $id = null, ?string $type = null - ): string - { + ): string { /** @var Listener $def */ $def = $this->getAttributeDefinition($listener); $id ??= $def?->id ?? $this->getListenerId($listener); @@ -222,6 +221,24 @@ protected function getAttributes(string $attribute, \Reflector $ref): array return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); } + protected function deriveMethod(string $service): string + { + if (!class_exists($service)) { + throw ServiceRegistrationClassNotExists::create($service); + } + $rClass = new \ReflectionClass($service); + $rMethods = $rClass->getMethods(); + + // If the class has only one method, assume that's the listener. + // Otherwise, use __invoke if not otherwise specified. + // Otherwise, we cannot tell what to do so throw. + return match (true) { + count($rMethods) === 1 => $rMethods[0]->name, + $rClass->hasMethod('__invoke') => '__invoke', + default => throw ServiceRegistrationTooManyMethods::create($service), + }; + } + /** * Tries to get the type of a callable listener. * diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 4e127a7..fa59358 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -250,4 +251,63 @@ public function optimize_event_anonymous_class(): void self::assertEquals('BACD', implode($event->result())); } + + + + #[Test, DataProvider('detection_class_examples')] + public function detects_invoke_method_and_type(string $class): void + { + $builder = new ProviderBuilder(); + $container = new MockContainer(); + + $container->addService($class, new $class()); + + $builder->listenerService($class); + + $provider = $this->makeAnonymousProvider($builder, $container); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals($class, $event->result()[0]); + } + + public static function detection_class_examples(): iterable + { + return [ + [InvokableListener::class], + [ArbitraryListener::class], + [CompoundListener::class], + ]; + } + + #[Test] + public function rejects_multi_method_class_without_invoke(): void + { + $this->expectException(ServiceRegistrationTooManyMethods::class); + $container = new MockContainer(); + + $container->addService(InvalidListener::class, new InvalidListener()); + + $builder = new ProviderBuilder(); + + $builder->listenerService(InvalidListener::class); + } + + #[Test] + public function rejects_missing_auto_detected_service(): void + { + $this->expectException(ServiceRegistrationClassNotExists::class); + $container = new MockContainer(); + + $provider = new OrderedListenerProvider($container); + + $builder = new ProviderBuilder(); + + /** @phpsan-ignore-next-line */ + $builder->listenerService(DoesNotExist::class); + } } diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 08103c7..5cce7d7 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -273,16 +274,16 @@ public function malformed_subscriber_manual_after_fails(): void MockMalformedSubscriber::registerListenersAfter($proxy); } - #[Test] - public function detects_invoke_method_and_type(): void + #[Test, DataProvider('detection_class_examples')] + public function detects_invoke_method_and_type(string $class): void { $container = new MockContainer(); - $container->addService(InvokableListener::class, new InvokableListener()); + $container->addService($class, new $class()); $provider = new OrderedListenerProvider($container); - $provider->listenerService(InvokableListener::class); + $provider->listenerService($class); $event = new CollectingEvent(); @@ -290,47 +291,16 @@ public function detects_invoke_method_and_type(): void $listener($event); } - self::assertEquals(InvokableListener::class, $event->result()[0]); + self::assertEquals($class, $event->result()[0]); } - #[Test] - public function detects_arbitrary_method_and_type(): void + public static function detection_class_examples(): iterable { - $container = new MockContainer(); - - $container->addService(ArbitraryListener::class, new ArbitraryListener()); - - $provider = new OrderedListenerProvider($container); - - $provider->listenerService(ArbitraryListener::class); - - $event = new CollectingEvent(); - - foreach ($provider->getListenersForEvent($event) as $listener) { - $listener($event); - } - - self::assertEquals(ArbitraryListener::class, $event->result()[0]); - } - - #[Test] - public function detects_compound_method_and_type(): void - { - $container = new MockContainer(); - - $container->addService(CompoundListener::class, new CompoundListener()); - - $provider = new OrderedListenerProvider($container); - - $provider->listenerService(CompoundListener::class); - - $event = new CollectingEvent(); - - foreach ($provider->getListenersForEvent($event) as $listener) { - $listener($event); - } - - self::assertEquals(CompoundListener::class, $event->result()[0]); + return [ + [InvokableListener::class], + [ArbitraryListener::class], + [CompoundListener::class], + ]; } #[Test] @@ -345,14 +315,13 @@ public function rejects_multi_method_class_without_invoke(): void $provider->listenerService(InvalidListener::class); } + #[Test] public function rejects_missing_auto_detected_service(): void { $this->expectException(ServiceRegistrationClassNotExists::class); $container = new MockContainer(); - $container->addService(InvalidListener::class, new InvalidListener()); - $provider = new OrderedListenerProvider($container); /** @phpsan-ignore-next-line */ From 5575936dd309eb47e14e77e67db4bee83f2d8876 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 16:29:46 -0600 Subject: [PATCH 51/77] PHPStan fixes (well, workarounds) --- phpstan.neon | 3 +++ src/OrderedListenerProvider.php | 1 + src/ProviderBuilder.php | 1 + tests/CompiledListenerProviderTest.php | 2 +- tests/OrderedListenerProviderServiceTest.php | 2 +- 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 2fcd6d2..0865407 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,3 +11,6 @@ parameters: - message: '#type has no value type specified in iterable type iterable#' path: tests/ + # PHPStan is overly aggressive on readonly properties. + - '#Class (.*) has an uninitialized readonly property (.*). Assign it in the constructor.#' + - '#Readonly property (.*) is assigned outside of the constructor.#' diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 1007663..c1b0d0e 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -48,6 +48,7 @@ public function listenerService( if (!class_exists($service)) { throw ServiceRegistrationClassNotExists::create($service); } + // @phpstan-ignore-next-line $type = $this->getParameterType([$service, $method]); } diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 72ba697..9b5a3c7 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -49,6 +49,7 @@ public function listenerService( if (!class_exists($service)) { throw ServiceRegistrationClassNotExists::create($service); } + // @phpstan-ignore-next-line $type = $this->getParameterType([$service, $method]); } diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index fa59358..bb98178 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -307,7 +307,7 @@ public function rejects_missing_auto_detected_service(): void $builder = new ProviderBuilder(); - /** @phpsan-ignore-next-line */ + // @phpstan-ignore-next-line $builder->listenerService(DoesNotExist::class); } } diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 5cce7d7..878ba15 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -324,7 +324,7 @@ public function rejects_missing_auto_detected_service(): void $provider = new OrderedListenerProvider($container); - /** @phpsan-ignore-next-line */ + /** @phpstan-ignore-next-line */ $provider->listenerService(DoesNotExist::class); } From e3427c8c130f28e41c0416690bc85ab6dd5744ee Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 29 Dec 2023 16:39:59 -0600 Subject: [PATCH 52/77] Ignore IDE files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aa5090d..0a4b7e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ vendor .phpunit.cache /vendor-bin/**/vendor .env +.idea From ec0da863eb9cfd83efceb8502f4a4d9706aae433 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 31 Dec 2023 12:08:20 -0600 Subject: [PATCH 53/77] Support attributes on class-as-service-id compiled cases. --- src/ProviderBuilder.php | 21 +++++++++++++++++++++ src/ProviderCollector.php | 8 ++++---- tests/CompiledListenerProviderTest.php | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 9b5a3c7..f4a7468 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -53,6 +53,27 @@ public function listenerService( $type = $this->getParameterType([$service, $method]); } + // In the special case that the service is the class name, we can + // leverage attributes. + if (class_exists($service)) { + $listener = [$service, $method]; + /** @var Listener $def */ + $def = $this->getAttributeDefinition($listener); + $id ??= $def?->id ?? $this->getListenerId($listener); + $type ??= $def?->type ?? $this->getType($listener); + + // If any ordering is specified explicitly, that completely overrules any + // attributes. + if (!is_null($priority) || $before || $after) { + $def->priority = $priority; + $def->before = $before; + $def->after = $after; + } + + $entry = new ListenerServiceEntry($service, $method, $type); + return $this->listeners->add($entry, $id, priority: $def->priority, before: $def->before, after: $def->after); + } + $entry = new ListenerServiceEntry($service, $method, $type); $id ??= $service . '-' . $method; diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 9026fb9..d68a289 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -153,7 +153,7 @@ protected function addSubscribersByProxy(string $class, string $service): Listen return $proxy; } - protected function getAttributeDefinition(callable $listener): Listener + protected function getAttributeDefinition(callable|array $listener): Listener { $ref = null; @@ -274,13 +274,13 @@ protected function getType(callable $listener): string * generate a random ID if necessary. It will also handle duplicates * for us. This method is just a suggestion. * - * @param callable $listener + * @param callable|array $listener * The listener for which to derive an ID. * * @return string|null * The derived ID if possible or null if no reasonable ID could be derived. */ - protected function getListenerId(callable $listener): ?string + protected function getListenerId(callable|array $listener): ?string { if ($this->isFunctionCallable($listener)) { // Function callables are strings, so use that directly. @@ -307,7 +307,7 @@ protected function getListenerId(callable $listener): ?string * @return bool * True if the callable represents a function, false otherwise. */ - protected function isFunctionCallable(callable $callable): bool + protected function isFunctionCallable(callable|array $callable): bool { // We can't check for function_exists() because it may be included later by the time it matters. return is_string($callable); diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index bb98178..ac9d6ed 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -310,4 +310,26 @@ public function rejects_missing_auto_detected_service(): void // @phpstan-ignore-next-line $builder->listenerService(DoesNotExist::class); } + + #[Test] + public function add_attribute_based_service_methods(): void + { + $builder = new ProviderBuilder(); + $container = new MockContainer(); + + $container->addService(TestAttributedListeners::class, new TestAttributedListeners()); + + $builder->listenerService(TestAttributedListeners::class, 'listenerC'); + $builder->listenerService(TestAttributedListeners::class, 'listenerD'); + + $provider = $this->makeAnonymousProvider($builder, $container); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals('DC', implode($event->result())); + } } From 258b6b111cb65e21daf5400ebc588c655776159c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 31 Dec 2023 12:29:28 -0600 Subject: [PATCH 54/77] Add attribute support to class-is-service-id non-compiled. --- src/OrderedListenerProvider.php | 20 ++++++ tests/MockAttributeSubscriber.php | 61 +++++++++++++++++++ .../OrderedListenerProviderAttributeTest.php | 21 +++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/MockAttributeSubscriber.php diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index c1b0d0e..bba4e9d 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -52,6 +52,26 @@ public function listenerService( $type = $this->getParameterType([$service, $method]); } + // In the special case that the service is the class name, we can + // leverage attributes. + if (class_exists($service)) { + $listener = [$service, $method]; + /** @var Listener $def */ + $def = $this->getAttributeDefinition($listener); + $id ??= $def?->id ?? $this->getListenerId($listener); + $type ??= $def?->type ?? $this->getType($listener); + + // If any ordering is specified explicitly, that completely overrules any + // attributes. + if (!is_null($priority) || $before || $after) { + $def->priority = $priority; + $def->before = $before; + $def->after = $after; + } + return $this->listener($this->makeListenerForService($service, $method), priority: $def->priority, before: $def->before, after: $def->after, id: $id, type: $type); + } + + $id ??= $service . '-' . $method; return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } diff --git a/tests/MockAttributeSubscriber.php b/tests/MockAttributeSubscriber.php new file mode 100644 index 0000000..77f6308 --- /dev/null +++ b/tests/MockAttributeSubscriber.php @@ -0,0 +1,61 @@ +add('A'); + } + + #[ListenerPriority(5)] + public function onB(CollectingEvent $event) : void + { + $event->add('B'); + } + + #[ListenerBefore(__CLASS__ . '-' . 'onA')] + public function onC(CollectingEvent $event) : void + { + $event->add('C'); + } + + #[ListenerAfter(__CLASS__ . '-' . 'onA')] + public function onD(CollectingEvent $event) : void + { + $event->add('D'); + } + + public function onE(CollectingEvent $event) : void + { + $event->add('E'); + } + + #[ListenerPriority(-5)] + public function notNormalName(CollectingEvent $event) : void + { + $event->add('F'); + } + + // @phpstan-ignore-next-line + public function onG(NoEvent $event) : void + { + // @phpstan-ignore-next-line + $event->add('G'); + } + + public function ignoredMethodThatDoesNothing() : void + { + throw new \Exception('What are you doing here?'); + } + + public function ignoredMethodWithOnInTheName_on() : void + { + throw new \Exception('What are you doing here?'); + } +} + diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index 9b1ad77..f3fad2b 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -190,4 +190,25 @@ public function before_after_methods_win_over_attributes(): void self::assertEquals('CAD', implode($event->result())); } + + #[Test] + public function add_attribute_based_service_methods(): void + { + $container = new MockContainer(); + + $container->addService(TestAttributedListeners::class, new TestAttributedListeners()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(TestAttributedListeners::class, 'listenerC'); + $provider->listenerService(TestAttributedListeners::class, 'listenerD'); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + self::assertEquals('DC', implode($event->result())); + } } From ea8680cfcb6768a1ba07b51e386012914c034d14 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 31 Dec 2023 12:38:41 -0600 Subject: [PATCH 55/77] Move mocks and fakes into dedicated namespaces for better organization. --- benchmarks/CompiledProviderBench.php | 10 +-- benchmarks/OptimizedCompiledProviderBench.php | 4 +- benchmarks/OrderedProviderBench.php | 2 - benchmarks/ProviderBenchBase.php | 2 +- tests/CallbackProviderTest.php | 43 +++--------- .../CompiledListenerProviderAttributeTest.php | 5 ++ ...ompiledListenerProviderInheritanceTest.php | 69 +++++-------------- tests/CompiledListenerProviderTest.php | 33 ++++----- tests/DebugEventDispatcherTest.php | 4 +- tests/DispatcherTest.php | 5 +- tests/{ => Events}/CollectingEvent.php | 2 +- tests/Events/DoNothingEvent.php | 8 +++ tests/{ => Events}/DummyEvent.php | 2 +- tests/Events/EventOne.php | 7 ++ tests/Events/EventTwo.php | 7 ++ tests/Events/LifecycleEvent.php | 21 ++++++ tests/Events/LoadEvent.php | 9 +++ tests/Events/SaveEvent.php | 9 +++ .../{ => Events}/StoppableCollectingEvent.php | 2 +- tests/Fakes/EventParentInterface.php | 10 +++ tests/Fakes/ListenedDirectly.php | 18 +++++ tests/{ => Fakes}/MockContainer.php | 2 +- tests/{ => Fakes}/MockLogger.php | 2 +- tests/Fakes/NotListenedDirectly.php | 18 +++++ tests/Fakes/Subclass.php | 9 +++ tests/Listeners/ArbitraryListener.php | 13 ++++ tests/Listeners/CompoundListener.php | 18 +++++ tests/Listeners/InvalidListener.php | 18 +++++ tests/Listeners/InvokableListener.php | 13 ++++ tests/Listeners/Listen.php | 13 ++++ tests/Listeners/ListenService.php | 13 ++++ .../MockAttributedSubscriber.php | 9 ++- .../MockMalformedSubscriber.php | 5 +- tests/{ => Listeners}/MockSubscriber.php | 7 +- tests/Listeners/TestAttributedListeners.php | 34 +++++++++ tests/Listeners/TestListeners.php | 28 ++++++++ tests/MockAttributeSubscriber.php | 61 ---------------- ...edListenerProviderAttributeServiceTest.php | 3 + .../OrderedListenerProviderAttributeTest.php | 44 ++---------- tests/OrderedListenerProviderIdTest.php | 32 ++------- ...eredListenerProviderMultiAttributeTest.php | 1 + tests/OrderedListenerProviderServiceTest.php | 55 +++------------ tests/OrderedListenerProviderTest.php | 12 ++-- 43 files changed, 375 insertions(+), 307 deletions(-) rename tests/{ => Events}/CollectingEvent.php (89%) create mode 100644 tests/Events/DoNothingEvent.php rename tests/{ => Events}/DummyEvent.php (65%) create mode 100644 tests/Events/EventOne.php create mode 100644 tests/Events/EventTwo.php create mode 100644 tests/Events/LifecycleEvent.php create mode 100644 tests/Events/LoadEvent.php create mode 100644 tests/Events/SaveEvent.php rename tests/{ => Events}/StoppableCollectingEvent.php (93%) create mode 100644 tests/Fakes/EventParentInterface.php create mode 100644 tests/Fakes/ListenedDirectly.php rename tests/{ => Fakes}/MockContainer.php (96%) rename tests/{ => Fakes}/MockLogger.php (93%) create mode 100644 tests/Fakes/NotListenedDirectly.php create mode 100644 tests/Fakes/Subclass.php create mode 100644 tests/Listeners/ArbitraryListener.php create mode 100644 tests/Listeners/CompoundListener.php create mode 100644 tests/Listeners/InvalidListener.php create mode 100644 tests/Listeners/InvokableListener.php create mode 100644 tests/Listeners/Listen.php create mode 100644 tests/Listeners/ListenService.php rename tests/{ => Listeners}/MockAttributedSubscriber.php (84%) rename tests/{ => Listeners}/MockMalformedSubscriber.php (93%) rename tests/{ => Listeners}/MockSubscriber.php (90%) create mode 100644 tests/Listeners/TestAttributedListeners.php create mode 100644 tests/Listeners/TestListeners.php delete mode 100644 tests/MockAttributeSubscriber.php diff --git a/benchmarks/CompiledProviderBench.php b/benchmarks/CompiledProviderBench.php index 96682d9..ce4f0be 100644 --- a/benchmarks/CompiledProviderBench.php +++ b/benchmarks/CompiledProviderBench.php @@ -4,19 +4,13 @@ namespace Crell\Tukio\Benchmarks; -use Crell\Tukio\CollectingEvent; -use Crell\Tukio\DummyEvent; -use Crell\Tukio\MockContainer; +use Crell\Tukio\Events\DummyEvent; +use Crell\Tukio\Fakes\MockContainer; use Crell\Tukio\ProviderBuilder; use Crell\Tukio\ProviderCompiler; use PhpBench\Benchmark\Metadata\Annotations\AfterClassMethods; use PhpBench\Benchmark\Metadata\Annotations\BeforeClassMethods; use PhpBench\Benchmark\Metadata\Annotations\Groups; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\OutputTimeUnit; -use PhpBench\Benchmark\Metadata\Annotations\RetryThreshold; -use PhpBench\Benchmark\Metadata\Annotations\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Warmup; /** * @Groups({"Providers"}) diff --git a/benchmarks/OptimizedCompiledProviderBench.php b/benchmarks/OptimizedCompiledProviderBench.php index 5399149..68856ef 100644 --- a/benchmarks/OptimizedCompiledProviderBench.php +++ b/benchmarks/OptimizedCompiledProviderBench.php @@ -4,8 +4,8 @@ namespace Crell\Tukio\Benchmarks; -use Crell\Tukio\CollectingEvent; -use Crell\Tukio\DummyEvent; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Events\DummyEvent; use PhpBench\Benchmark\Metadata\Annotations\Groups; /** diff --git a/benchmarks/OrderedProviderBench.php b/benchmarks/OrderedProviderBench.php index 64b33d7..c3b6b5d 100644 --- a/benchmarks/OrderedProviderBench.php +++ b/benchmarks/OrderedProviderBench.php @@ -4,10 +4,8 @@ namespace Crell\Tukio\Benchmarks; -use Crell\Tukio\CollectingEvent; use Crell\Tukio\OrderedListenerProvider; use PhpBench\Benchmark\Metadata\Annotations\Groups; -use Psr\EventDispatcher\ListenerProviderInterface; /** * @Groups({"Providers"}) diff --git a/benchmarks/ProviderBenchBase.php b/benchmarks/ProviderBenchBase.php index 40db072..4280f5c 100644 --- a/benchmarks/ProviderBenchBase.php +++ b/benchmarks/ProviderBenchBase.php @@ -4,7 +4,7 @@ namespace Crell\Tukio\Benchmarks; -use Crell\Tukio\CollectingEvent; +use Crell\Tukio\Events\CollectingEvent; use PhpBench\Benchmark\Metadata\Annotations\Groups; use PhpBench\Benchmark\Metadata\Annotations\Iterations; use PhpBench\Benchmark\Metadata\Annotations\OutputTimeUnit; diff --git a/tests/CallbackProviderTest.php b/tests/CallbackProviderTest.php index eb3f4bb..9e55cec 100644 --- a/tests/CallbackProviderTest.php +++ b/tests/CallbackProviderTest.php @@ -5,38 +5,18 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; - -class LifecycleEvent extends CollectingEvent implements CallbackEventInterface -{ - protected FakeEntity $entity; - - public function __construct(FakeEntity $entity) - { - $this->entity = $entity; - } - - public function getSubject(): object - { - return $this->entity; - } -} - -class LoadEvent extends LifecycleEvent {} - -class SaveEvent extends LifecycleEvent {} - class FakeEntity { - - public function load(LoadEvent $event): void + public function load(Events\LoadEvent $event): void { $event->add('A'); } - public function save(SaveEvent $event): void + public function save(Events\SaveEvent $event): void { $event->add('B'); } @@ -48,13 +28,12 @@ public function stuff(StuffEvent $event): void $event->add('C'); } - public function all(LifecycleEvent $event): void + public function all(Events\LifecycleEvent $event): void { $event->add('D'); } } - class CallbackProviderTest extends TestCase { @@ -65,11 +44,11 @@ public function callback_provider(): void $entity = new FakeEntity(); - $p->addCallbackMethod(LoadEvent::class, 'load'); - $p->addCallbackMethod(SaveEvent::class, 'save'); - $p->addCallbackMethod(LifecycleEvent::class, 'all'); + $p->addCallbackMethod(Events\LoadEvent::class, 'load'); + $p->addCallbackMethod(Events\SaveEvent::class, 'save'); + $p->addCallbackMethod(Events\LifecycleEvent::class, 'all'); - $event = new LoadEvent($entity); + $event = new Events\LoadEvent($entity); foreach ($p->getListenersForEvent($event) as $listener) { $listener($event); @@ -83,9 +62,9 @@ public function non_callback_event_skips_silently(): void { $p = new CallbackProvider(); - $p->addCallbackMethod(LoadEvent::class, 'load'); - $p->addCallbackMethod(SaveEvent::class, 'save'); - $p->addCallbackMethod(LifecycleEvent::class, 'all'); + $p->addCallbackMethod(Events\LoadEvent::class, 'load'); + $p->addCallbackMethod(Events\SaveEvent::class, 'save'); + $p->addCallbackMethod(Events\LifecycleEvent::class, 'all'); $event = new CollectingEvent(); diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index 41b93dc..bbaacd3 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -4,6 +4,11 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Events\EventOne; +use Crell\Tukio\Fakes\MockContainer; +use Crell\Tukio\Listeners\MockAttributedSubscriber; +use Crell\Tukio\Listeners\MockSubscriber; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; diff --git a/tests/CompiledListenerProviderInheritanceTest.php b/tests/CompiledListenerProviderInheritanceTest.php index 36fd9cf..0292282 100644 --- a/tests/CompiledListenerProviderInheritanceTest.php +++ b/tests/CompiledListenerProviderInheritanceTest.php @@ -4,61 +4,26 @@ namespace Crell\Tukio; +use Crell\Tukio\Fakes\EventParentInterface; +use Crell\Tukio\Fakes\MockContainer; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -interface EventParentInterface -{ - public function add(string $val): void; - public function result(): array; -} - -class ListenedDirectly implements EventParentInterface { - protected array $out = []; - - public function add(string $val): void - { - $this->out[] = $val; - } - - public function result() : array - { - return $this->out; - } -} - -class Subclass extends ListenedDirectly {} - -class NotListenedDirectly implements EventParentInterface { - protected array $out = []; - - public function add(string $val): void - { - $this->out[] = $val; - } - - public function result(): array - { - return $this->out; - } -} - -function inheritanceListenerA(EventParentInterface $event): void +function inheritanceListenerA(Fakes\EventParentInterface $event): void { $event->add('A'); } -function inheritanceListenerB(ListenedDirectly $event): void +function inheritanceListenerB(Fakes\ListenedDirectly $event): void { $event->add('B'); } -function inheritanceListenerC(Subclass $event): void +function inheritanceListenerC(Fakes\Subclass $event): void { $event->add('C'); } - class CompiledListenerProviderInheritanceTest extends TestCase { use MakeCompiledProviderTrait; @@ -78,13 +43,13 @@ public function interface_listener_catches_everything(): void $provider = $this->makeProvider($builder, $container, $class, $namespace); $tests = [ - ListenedDirectly::class => 'A', - Subclass::class => 'A', - NotListenedDirectly::class => 'A', + Fakes\ListenedDirectly::class => 'A', + Fakes\Subclass::class => 'A', + Fakes\NotListenedDirectly::class => 'A', ]; foreach ($tests as $class => $result) { - /** @var EventParentInterface $event */ + /** @var \Crell\Tukio\Fakes\EventParentInterface $event */ $event = new $class(); foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); @@ -108,13 +73,13 @@ public function class_listener_catches_subclass(): void $provider = $this->makeProvider($builder, $container, $class, $namespace); $tests = [ - ListenedDirectly::class => 'B', - Subclass::class => 'B', - NotListenedDirectly::class => '', + Fakes\ListenedDirectly::class => 'B', + Fakes\Subclass::class => 'B', + Fakes\NotListenedDirectly::class => '', ]; foreach ($tests as $class => $result) { - /** @var EventParentInterface $event */ + /** @var \Crell\Tukio\Fakes\EventParentInterface $event */ $event = new $class(); foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); @@ -138,13 +103,13 @@ public function subclass_listener_catches_subclass(): void $provider = $this->makeProvider($builder, $container, $class, $namespace); $tests = [ - ListenedDirectly::class => '', - Subclass::class => 'C', - NotListenedDirectly::class => '', + Fakes\ListenedDirectly::class => '', + Fakes\Subclass::class => 'C', + Fakes\NotListenedDirectly::class => '', ]; foreach ($tests as $class => $result) { - /** @var EventParentInterface $event */ + /** @var \Crell\Tukio\Fakes\EventParentInterface $event */ $event = new $class(); foreach ($provider->getListenersForEvent($event) as $listener) { $listener($event); diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index ac9d6ed..96a875b 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -4,6 +4,15 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Events\EventOne; +use Crell\Tukio\Fakes\MockContainer; +use Crell\Tukio\Listeners\ArbitraryListener; +use Crell\Tukio\Listeners\CompoundListener; +use Crell\Tukio\Listeners\InvalidListener; +use Crell\Tukio\Listeners\InvokableListener; +use Crell\Tukio\Listeners\MockSubscriber; +use Crell\Tukio\Listeners\TestAttributedListeners; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -26,22 +35,6 @@ function noListen(EventOne $event): void throw new \Exception('This should not be called'); } -class Listen -{ - public static function listen(CollectingEvent $event): void - { - $event->add('C'); - } -} - -class ListenService -{ - public static function listen(CollectingEvent $event): void - { - $event->add('D'); - } -} - class CompiledListenerProviderTest extends TestCase { use MakeCompiledProviderTrait; @@ -55,12 +48,12 @@ public function compiled_provider_triggers_in_order(): void $builder = new ProviderBuilder(); $container = new MockContainer(); - $container->addService('D', new ListenService()); + $container->addService('D', new Listeners\ListenService()); $builder->addListener('\\Crell\\Tukio\\listenerA'); $builder->addListener('\\Crell\\Tukio\\listenerB'); $builder->addListener('\\Crell\\Tukio\\noListen'); - $builder->addListener([Listen::class, 'listen']); + $builder->addListener([Listeners\Listen::class, 'listen']); $builder->addListenerService('D', 'listen', CollectingEvent::class); $provider = $this->makeProvider($builder, $container, $class, $namespace); @@ -202,12 +195,12 @@ public function anonymous_class_compile(): void $builder = new ProviderBuilder(); $container = new MockContainer(); - $container->addService('D', new ListenService()); + $container->addService('D', new Listeners\ListenService()); $builder->addListener('\\Crell\\Tukio\\listenerA'); $builder->addListener('\\Crell\\Tukio\\listenerB'); $builder->addListener('\\Crell\\Tukio\\noListen'); - $builder->addListener([Listen::class, 'listen']); + $builder->addListener([Listeners\Listen::class, 'listen']); $builder->addListenerService('D', 'listen', CollectingEvent::class); $provider = $this->makeAnonymousProvider($builder, $container); diff --git a/tests/DebugEventDispatcherTest.php b/tests/DebugEventDispatcherTest.php index a2d7a61..56d7814 100644 --- a/tests/DebugEventDispatcherTest.php +++ b/tests/DebugEventDispatcherTest.php @@ -5,11 +5,11 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Fakes\MockLogger; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Log\AbstractLogger; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; class DebugEventDispatcherTest extends TestCase diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index c1648c3..28f016a 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,11 +4,12 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Events\StoppableCollectingEvent; +use Crell\Tukio\Fakes\MockLogger; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\ListenerProviderInterface; -use Psr\Log\AbstractLogger; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; class DispatcherTest extends TestCase diff --git a/tests/CollectingEvent.php b/tests/Events/CollectingEvent.php similarity index 89% rename from tests/CollectingEvent.php rename to tests/Events/CollectingEvent.php index 63e8865..da2ea4f 100644 --- a/tests/CollectingEvent.php +++ b/tests/Events/CollectingEvent.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Events; class CollectingEvent diff --git a/tests/Events/DoNothingEvent.php b/tests/Events/DoNothingEvent.php new file mode 100644 index 0000000..911c0d1 --- /dev/null +++ b/tests/Events/DoNothingEvent.php @@ -0,0 +1,8 @@ +entity = $entity; + } + + public function getSubject(): object + { + return $this->entity; + } +} diff --git a/tests/Events/LoadEvent.php b/tests/Events/LoadEvent.php new file mode 100644 index 0000000..54d5c00 --- /dev/null +++ b/tests/Events/LoadEvent.php @@ -0,0 +1,9 @@ +out[] = $val; + } + + public function result(): array + { + return $this->out; + } +} diff --git a/tests/MockContainer.php b/tests/Fakes/MockContainer.php similarity index 96% rename from tests/MockContainer.php rename to tests/Fakes/MockContainer.php index 0e6756f..06f61dd 100644 --- a/tests/MockContainer.php +++ b/tests/Fakes/MockContainer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Fakes; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; diff --git a/tests/MockLogger.php b/tests/Fakes/MockLogger.php similarity index 93% rename from tests/MockLogger.php rename to tests/Fakes/MockLogger.php index ede5387..f4ca2b5 100644 --- a/tests/MockLogger.php +++ b/tests/Fakes/MockLogger.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Fakes; use Psr\Log\AbstractLogger; diff --git a/tests/Fakes/NotListenedDirectly.php b/tests/Fakes/NotListenedDirectly.php new file mode 100644 index 0000000..fcabae7 --- /dev/null +++ b/tests/Fakes/NotListenedDirectly.php @@ -0,0 +1,18 @@ +out[] = $val; + } + + public function result(): array + { + return $this->out; + } +} diff --git a/tests/Fakes/Subclass.php b/tests/Fakes/Subclass.php new file mode 100644 index 0000000..63a7870 --- /dev/null +++ b/tests/Fakes/Subclass.php @@ -0,0 +1,9 @@ +add(static::class); + } +} diff --git a/tests/Listeners/CompoundListener.php b/tests/Listeners/CompoundListener.php new file mode 100644 index 0000000..97023a6 --- /dev/null +++ b/tests/Listeners/CompoundListener.php @@ -0,0 +1,18 @@ +add(static::class); + } + + public function dontUseThis(CollectingEvent $event): void + { + throw new \Exception('This should not get called.'); + } +} diff --git a/tests/Listeners/InvalidListener.php b/tests/Listeners/InvalidListener.php new file mode 100644 index 0000000..bb3e110 --- /dev/null +++ b/tests/Listeners/InvalidListener.php @@ -0,0 +1,18 @@ +add(static::class); + } + + public function dontUseThis(CollectingEvent $event): void + { + throw new \Exception('This should not get called.'); + } +} diff --git a/tests/Listeners/InvokableListener.php b/tests/Listeners/InvokableListener.php new file mode 100644 index 0000000..eefe2e2 --- /dev/null +++ b/tests/Listeners/InvokableListener.php @@ -0,0 +1,13 @@ +add(static::class); + } +} diff --git a/tests/Listeners/Listen.php b/tests/Listeners/Listen.php new file mode 100644 index 0000000..82fcfec --- /dev/null +++ b/tests/Listeners/Listen.php @@ -0,0 +1,13 @@ +add('C'); + } +} diff --git a/tests/Listeners/ListenService.php b/tests/Listeners/ListenService.php new file mode 100644 index 0000000..faecded --- /dev/null +++ b/tests/Listeners/ListenService.php @@ -0,0 +1,13 @@ +add('D'); + } +} diff --git a/tests/MockAttributedSubscriber.php b/tests/Listeners/MockAttributedSubscriber.php similarity index 84% rename from tests/MockAttributedSubscriber.php rename to tests/Listeners/MockAttributedSubscriber.php index 7c96810..edf0527 100644 --- a/tests/MockAttributedSubscriber.php +++ b/tests/Listeners/MockAttributedSubscriber.php @@ -2,9 +2,16 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Listeners; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Listener; +use Crell\Tukio\ListenerAfter; +use Crell\Tukio\ListenerBefore; +use Crell\Tukio\ListenerPriority; +use Crell\Tukio\NoEvent; + class MockAttributedSubscriber { #[Listener(id: 'a')] diff --git a/tests/MockMalformedSubscriber.php b/tests/Listeners/MockMalformedSubscriber.php similarity index 93% rename from tests/MockMalformedSubscriber.php rename to tests/Listeners/MockMalformedSubscriber.php index 569a0a6..cf277d7 100644 --- a/tests/MockMalformedSubscriber.php +++ b/tests/Listeners/MockMalformedSubscriber.php @@ -2,7 +2,10 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Listeners; + +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\ListenerProxy; class MockMalformedSubscriber { diff --git a/tests/MockSubscriber.php b/tests/Listeners/MockSubscriber.php similarity index 90% rename from tests/MockSubscriber.php rename to tests/Listeners/MockSubscriber.php index 3a33100..ab58a20 100644 --- a/tests/MockSubscriber.php +++ b/tests/Listeners/MockSubscriber.php @@ -2,9 +2,14 @@ declare(strict_types=1); -namespace Crell\Tukio; +namespace Crell\Tukio\Listeners; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\ListenerProxy; +use Crell\Tukio\NoEvent; +use Crell\Tukio\SubscriberInterface; + class MockSubscriber implements SubscriberInterface { public function onA(CollectingEvent $event) : void diff --git a/tests/Listeners/TestAttributedListeners.php b/tests/Listeners/TestAttributedListeners.php new file mode 100644 index 0000000..9f6e0d5 --- /dev/null +++ b/tests/Listeners/TestAttributedListeners.php @@ -0,0 +1,34 @@ +add('A'); + } + + #[ListenerBefore(before: 'a')] + public static function listenerB(CollectingEvent $event): void + { + $event->add('B'); + } + + #[ListenerPriority(id: 'c', priority: -4)] + public function listenerC(CollectingEvent $event): void + { + $event->add('C'); + } + + #[ListenerBefore(before: 'c')] + public function listenerD(CollectingEvent $event): void + { + $event->add('D'); + } +} diff --git a/tests/Listeners/TestListeners.php b/tests/Listeners/TestListeners.php new file mode 100644 index 0000000..e48f7ba --- /dev/null +++ b/tests/Listeners/TestListeners.php @@ -0,0 +1,28 @@ +add('A'); + } + + public static function listenerB(CollectingEvent $event): void + { + $event->add('B'); + } + + public function listenerC(CollectingEvent $event): void + { + $event->add('C'); + } + + public function listenerD(CollectingEvent $event): void + { + $event->add('D'); + } +} diff --git a/tests/MockAttributeSubscriber.php b/tests/MockAttributeSubscriber.php deleted file mode 100644 index 77f6308..0000000 --- a/tests/MockAttributeSubscriber.php +++ /dev/null @@ -1,61 +0,0 @@ -add('A'); - } - - #[ListenerPriority(5)] - public function onB(CollectingEvent $event) : void - { - $event->add('B'); - } - - #[ListenerBefore(__CLASS__ . '-' . 'onA')] - public function onC(CollectingEvent $event) : void - { - $event->add('C'); - } - - #[ListenerAfter(__CLASS__ . '-' . 'onA')] - public function onD(CollectingEvent $event) : void - { - $event->add('D'); - } - - public function onE(CollectingEvent $event) : void - { - $event->add('E'); - } - - #[ListenerPriority(-5)] - public function notNormalName(CollectingEvent $event) : void - { - $event->add('F'); - } - - // @phpstan-ignore-next-line - public function onG(NoEvent $event) : void - { - // @phpstan-ignore-next-line - $event->add('G'); - } - - public function ignoredMethodThatDoesNothing() : void - { - throw new \Exception('What are you doing here?'); - } - - public function ignoredMethodWithOnInTheName_on() : void - { - throw new \Exception('What are you doing here?'); - } -} - diff --git a/tests/OrderedListenerProviderAttributeServiceTest.php b/tests/OrderedListenerProviderAttributeServiceTest.php index 06f5d2c..4ea75e2 100644 --- a/tests/OrderedListenerProviderAttributeServiceTest.php +++ b/tests/OrderedListenerProviderAttributeServiceTest.php @@ -5,6 +5,9 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Fakes\MockContainer; +use Crell\Tukio\Listeners\MockAttributedSubscriber; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; diff --git a/tests/OrderedListenerProviderAttributeTest.php b/tests/OrderedListenerProviderAttributeTest.php index f3fad2b..b04e75c 100644 --- a/tests/OrderedListenerProviderAttributeTest.php +++ b/tests/OrderedListenerProviderAttributeTest.php @@ -4,6 +4,8 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Fakes\MockContainer; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -34,38 +36,6 @@ function at_listener_four($event): void $event->add('D'); } -class DoNothingEvent -{ - public bool $called = false; -} - -class TestAttributedListeners -{ - #[ListenerPriority(id: 'a', priority: -4)] - public static function listenerA(CollectingEvent $event) : void - { - $event->add('A'); - } - - #[ListenerBefore(before: 'a')] - public static function listenerB(CollectingEvent $event) : void - { - $event->add('B'); - } - - #[ListenerPriority(id: 'c', priority: -4)] - public function listenerC(CollectingEvent $event) : void - { - $event->add('C'); - } - - #[ListenerBefore(before: 'c')] - public function listenerD(CollectingEvent $event) : void - { - $event->add('D'); - } -} - class OrderedListenerProviderAttributeTest extends TestCase { #[Test] @@ -140,7 +110,7 @@ public function type_from_attribute_skips_correctly() : void $p->addListener("{$ns}at_listener_four"); - $event = new DoNothingEvent(); + $event = new Events\DoNothingEvent(); // This should explode with an "method not found" error // if the event is passed to the listener. @@ -156,7 +126,7 @@ public function attributes_found_on_object_methods() : void { $p = new OrderedListenerProvider(); - $object = new TestAttributedListeners(); + $object = new Listeners\TestAttributedListeners(); $p->addListener([$object, 'listenerC']); $p->addListener([$object, 'listenerD']); @@ -196,12 +166,12 @@ public function add_attribute_based_service_methods(): void { $container = new MockContainer(); - $container->addService(TestAttributedListeners::class, new TestAttributedListeners()); + $container->addService(Listeners\TestAttributedListeners::class, new Listeners\TestAttributedListeners()); $provider = new OrderedListenerProvider($container); - $provider->listenerService(TestAttributedListeners::class, 'listenerC'); - $provider->listenerService(TestAttributedListeners::class, 'listenerD'); + $provider->listenerService(Listeners\TestAttributedListeners::class, 'listenerC'); + $provider->listenerService(Listeners\TestAttributedListeners::class, 'listenerD'); $event = new CollectingEvent(); diff --git a/tests/OrderedListenerProviderIdTest.php b/tests/OrderedListenerProviderIdTest.php index 15a06d4..3a39500 100644 --- a/tests/OrderedListenerProviderIdTest.php +++ b/tests/OrderedListenerProviderIdTest.php @@ -5,6 +5,8 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Fakes\MockContainer; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -29,28 +31,6 @@ function event_listener_four(CollectingEvent $event): void } -class TestListeners -{ - public static function listenerA(CollectingEvent $event): void - { - $event->add('A'); - } - public static function listenerB(CollectingEvent $event): void - { - $event->add('B'); - } - - public function listenerC(CollectingEvent $event): void - { - $event->add('C'); - } - - public function listenerD(CollectingEvent $event): void - { - $event->add('D'); - } -} - class OrderedListenerProviderIdTest extends TestCase { @@ -81,8 +61,8 @@ public function natural_id_for_static_method(): void { $p = new OrderedListenerProvider(); - $p->addListener([TestListeners::class, 'listenerA'], -4); - $p->addListenerBefore(TestListeners::class . '::listenerA', [TestListeners::class, 'listenerB']); + $p->addListener([Listeners\TestListeners::class, 'listenerA'], -4); + $p->addListenerBefore(Listeners\TestListeners::class . '::listenerA', [Listeners\TestListeners::class, 'listenerB']); $event = new CollectingEvent(); @@ -98,10 +78,10 @@ public function natural_id_for_object_method(): void { $p = new OrderedListenerProvider(); - $l = new TestListeners(); + $l = new Listeners\TestListeners(); $p->addListener([$l, 'listenerC'], -4); - $p->addListenerBefore(TestListeners::class . '::listenerC', [$l, 'listenerD']); + $p->addListenerBefore(Listeners\TestListeners::class . '::listenerC', [$l, 'listenerD']); $event = new CollectingEvent(); diff --git a/tests/OrderedListenerProviderMultiAttributeTest.php b/tests/OrderedListenerProviderMultiAttributeTest.php index e3ef09e..02433b0 100644 --- a/tests/OrderedListenerProviderMultiAttributeTest.php +++ b/tests/OrderedListenerProviderMultiAttributeTest.php @@ -4,6 +4,7 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 878ba15..611ef74 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -4,51 +4,14 @@ namespace Crell\Tukio; +use Crell\Tukio\Events\CollectingEvent; +use Crell\Tukio\Fakes\MockContainer; +use Crell\Tukio\Listeners\MockMalformedSubscriber; +use Crell\Tukio\Listeners\MockSubscriber; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -class InvokableListener -{ - public function __invoke(CollectingEvent $event): void - { - $event->add(static::class); - } -} -class ArbitraryListener -{ - public function doStuff(CollectingEvent $event): void - { - $event->add(static::class); - } -} - -class CompoundListener -{ - public function __invoke(CollectingEvent $event): void - { - $event->add(static::class); - } - - public function dontUseThis(CollectingEvent $event): void - { - throw new \Exception('This should not get called.'); - } -} - -class InvalidListener -{ - public function useThis(CollectingEvent $event): void - { - $event->add(static::class); - } - - public function dontUseThis(CollectingEvent $event): void - { - throw new \Exception('This should not get called.'); - } -} - class OrderedListenerProviderServiceTest extends TestCase { protected MockContainer $mockContainer; @@ -297,9 +260,9 @@ public function detects_invoke_method_and_type(string $class): void public static function detection_class_examples(): iterable { return [ - [InvokableListener::class], - [ArbitraryListener::class], - [CompoundListener::class], + [Listeners\InvokableListener::class], + [Listeners\ArbitraryListener::class], + [Listeners\CompoundListener::class], ]; } @@ -309,11 +272,11 @@ public function rejects_multi_method_class_without_invoke(): void $this->expectException(ServiceRegistrationTooManyMethods::class); $container = new MockContainer(); - $container->addService(InvalidListener::class, new InvalidListener()); + $container->addService(Listeners\InvalidListener::class, new Listeners\InvalidListener()); $provider = new OrderedListenerProvider($container); - $provider->listenerService(InvalidListener::class); + $provider->listenerService(Listeners\InvalidListener::class); } #[Test] diff --git a/tests/OrderedListenerProviderTest.php b/tests/OrderedListenerProviderTest.php index eadbec6..820d114 100644 --- a/tests/OrderedListenerProviderTest.php +++ b/tests/OrderedListenerProviderTest.php @@ -4,14 +4,10 @@ namespace Crell\Tukio; - +use Crell\Tukio\Events\CollectingEvent; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -class EventOne extends CollectingEvent {} - -class EventTwo extends CollectingEvent {} - class OrderedListenerProviderTest extends TestCase { #[Test] @@ -19,13 +15,13 @@ public function only_type_correct_listeners_are_returned(): void { $p = new OrderedListenerProvider(); - $p->addListener(function (EventOne $event) { + $p->addListener(function (Events\EventOne $event) { $event->add('Y'); }); $p->addListener(function (CollectingEvent $event) { $event->add('Y'); }); - $p->addListener(function (EventTwo $event) { + $p->addListener(function (Events\EventTwo $event) { $event->add('N'); }); // This class doesn't exist but should not result in an error. @@ -35,7 +31,7 @@ public function only_type_correct_listeners_are_returned(): void $event->add('F'); }); - $event = new EventOne(); + $event = new Events\EventOne(); foreach ($p->getListenersForEvent($event) as $listener) { $listener($event); From 3821d1d5b17b660ed0febc3dc3a893744737dcac Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 31 Dec 2023 12:49:17 -0600 Subject: [PATCH 56/77] PHPStan fixes. --- src/OrderedListenerProvider.php | 1 - src/ProviderBuilder.php | 1 - src/ProviderCollector.php | 9 ++++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index bba4e9d..8d80a81 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -59,7 +59,6 @@ public function listenerService( /** @var Listener $def */ $def = $this->getAttributeDefinition($listener); $id ??= $def?->id ?? $this->getListenerId($listener); - $type ??= $def?->type ?? $this->getType($listener); // If any ordering is specified explicitly, that completely overrules any // attributes. diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index f4a7468..d943cda 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -60,7 +60,6 @@ public function listenerService( /** @var Listener $def */ $def = $this->getAttributeDefinition($listener); $id ??= $def?->id ?? $this->getListenerId($listener); - $type ??= $def?->type ?? $this->getType($listener); // If any ordering is specified explicitly, that completely overrules any // attributes. diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index d68a289..ce27568 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -153,6 +153,9 @@ protected function addSubscribersByProxy(string $class, string $service): Listen return $proxy; } + /** + * @param callable|array{0: string, 1: string} $listener + */ protected function getAttributeDefinition(callable|array $listener): Listener { $ref = null; @@ -160,12 +163,14 @@ protected function getAttributeDefinition(callable|array $listener): Listener if ($this->isFunctionCallable($listener)) { /** @var string $listener */ $ref = new \ReflectionFunction($listener); + // @phpstan-ignore-next-line } elseif ($this->isClassCallable($listener)) { // PHPStan says you cannot use array destructuring on a callable, but you can // if you know that it's an array (which in context we do). // @phpstan-ignore-next-line [$class, $method] = $listener; $ref = (new \ReflectionClass($class))->getMethod($method); + // @phpstan-ignore-next-line } elseif ($this->isObjectCallable($listener)) { // PHPStan says you cannot use array destructuring on a callable, but you can // if you know that it's an array (which in context we do). @@ -274,7 +279,7 @@ protected function getType(callable $listener): string * generate a random ID if necessary. It will also handle duplicates * for us. This method is just a suggestion. * - * @param callable|array $listener + * @param callable|array{0: string, 1: string} $listener * The listener for which to derive an ID. * * @return string|null @@ -287,6 +292,7 @@ protected function getListenerId(callable|array $listener): ?string // @phpstan-ignore-next-line return (string)$listener; } + // @phpstan-ignore-next-line if ($this->isClassCallable($listener)) { /** @var array{0: class-string, 1: string} $listener */ return $listener[0] . '::' . $listener[1]; @@ -304,6 +310,7 @@ protected function getListenerId(callable|array $listener): ?string * * Or at least a reasonable approximation, since a function name may not be defined yet. * + * @param callable|array{0: string, 1: string} $callable * @return bool * True if the callable represents a function, false otherwise. */ From c9f033ec2628944feb3535a6c21a8f9ae5c435b1 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:01:03 -0600 Subject: [PATCH 57/77] Use AttributeUtils for introspecting listeners. --- composer.json | 1 + src/Listener.php | 132 ++++++++++++++++-- src/ListenerAfter.php | 5 +- src/ListenerBefore.php | 5 +- src/ListenerPriority.php | 2 +- src/OrderedListenerProvider.php | 4 +- src/OrderedProviderInterface.php | 8 +- src/ProviderBuilder.php | 3 +- src/ProviderCollector.php | 98 ++++++++----- .../CompiledListenerProviderAttributeTest.php | 23 +-- tests/Listeners/AtListen.php | 15 ++ tests/Listeners/AtListenService.php | 13 ++ tests/Listeners/MockMalformedSubscriber.php | 4 + tests/OrderedListenerProviderServiceTest.php | 4 +- 14 files changed, 239 insertions(+), 78 deletions(-) create mode 100644 tests/Listeners/AtListen.php create mode 100644 tests/Listeners/AtListenService.php diff --git a/composer.json b/composer.json index 28969d5..c9cbd22 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ ], "require": { "php": "~8.1", + "crell/attributeutils": "dev-func-analyzer", "crell/ordered-collection": "v2.x-dev", "fig/event-dispatcher-util": "^1.3", "psr/container": "^1.0 || ^2.0", diff --git a/src/Listener.php b/src/Listener.php index b5b4384..e08cb38 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -5,13 +5,34 @@ namespace Crell\Tukio; use Attribute; +use Crell\AttributeUtils\Finalizable; +use Crell\AttributeUtils\FromReflectionMethod; +use Crell\AttributeUtils\HasSubAttributes; +use Crell\AttributeUtils\ParseMethods; +use Crell\AttributeUtils\ParseStaticMethods; +use Crell\AttributeUtils\ReadsClass; /** * The main attribute to customize a listener. */ #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] -class Listener implements ListenerAttribute +class Listener implements ListenerAttribute, HasSubAttributes, ParseMethods, ReadsClass, Finalizable, FromReflectionMethod, ParseStaticMethods { + /** + * @var Listener[] + * + * This is only used by the class-level attribute. When used on a method level it is ignored. + */ + public readonly array $methods; + + /** + * @var Listener[] + * + * This is only used by the class-level attribute. When used on a method level it is ignored. + */ + public readonly array $staticMethods; + + /** @var string[] */ public array $before = []; @@ -19,6 +40,13 @@ class Listener implements ListenerAttribute public array $after = []; public ?int $priority = null; + public readonly bool $hasDefinition; + + /** + * This is only meaningful on the method attribute. + */ + public readonly int $paramCount; + /** * @param ?string $id * The identifier by which this listener should be known. If not specified one will be generated. @@ -29,13 +57,88 @@ class Listener implements ListenerAttribute public function __construct( public ?string $id = null, public ?string $type = null, - ) {} + ) { + if ($id || $this->type) { + $this->hasDefinition ??= true; + } + } + + public function fromReflection(\ReflectionMethod $subject): void + { + $this->paramCount = $subject->getNumberOfRequiredParameters(); + if ($this->paramCount === 1) { + $params = $subject->getParameters(); + $this->type ??= $params[0]?->getType()?->getName(); + } + } + + /** + * This will only get called when this attribute is on a class. + * + * @param Listener[] $methods + */ + public function setMethods(array $methods): void + { + $this->methods = $methods; + } + + public function includeMethodsByDefault(): bool + { + return true; + } + + public function methodAttribute(): string + { + return __CLASS__; + } + + public function setStaticMethods(array $methods): void + { + $this->staticMethods = $methods; + } + + public function includeStaticMethodsByDefault(): bool + { + return true; + } + + public function staticMethodAttribute(): string + { + return __CLASS__; + } + + + /** + * This will only get called when this attribute is used on a method. + * + * @param Listener $class + */ + public function fromClassAttribute(object $class): void + { + $this->id ??= $class->id; + $this->type ??= $class->type; + $this->priority ??= $class->priority; + $this->before ??= $class->before; + $this->after ??= $class->after; + } + + public function subAttributes(): array + { + return [ + ListenerBefore::class => 'fromBefore', + ListenerAfter::class => 'fromAfter', + ListenerPriority::class => 'fromPriority', + ]; + } /** * @param array $attribs */ - public function absorbBefore(array $attribs): void + public function fromBefore(array $attribs): void { + if ($attribs) { + $this->hasDefinition ??= true; + } foreach ($attribs as $attrib) { $this->id ??= $attrib->id; $this->type ??= $attrib->type; @@ -46,8 +149,11 @@ public function absorbBefore(array $attribs): void /** * @param array $attribs */ - public function absorbAfter(array $attribs): void + public function fromAfter(array $attribs): void { + if ($attribs) { + $this->hasDefinition ??= true; + } foreach ($attribs as $attrib) { $this->id ??= $attrib->id; $this->type ??= $attrib->type; @@ -55,10 +161,20 @@ public function absorbAfter(array $attribs): void } } - public function absorbPriority(ListenerPriority $attrib): void + public function fromPriority(?ListenerPriority $attrib): void + { + if ($attrib) { + $this->hasDefinition ??= true; + } + $this->id ??= $attrib?->id; + $this->type ??= $attrib?->type; + $this->priority = $attrib?->priority; + } + + public function finalize(): void { - $this->id ??= $attrib->id; - $this->type ??= $attrib->type; - $this->priority = $attrib->priority; + $this->methods ??= []; + $this->hasDefinition ??= false; } + } diff --git a/src/ListenerAfter.php b/src/ListenerAfter.php index ef5e1e5..84e6f1b 100644 --- a/src/ListenerAfter.php +++ b/src/ListenerAfter.php @@ -5,9 +5,10 @@ namespace Crell\Tukio; use Attribute; +use Crell\AttributeUtils\Multivalue; -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerAfter implements ListenerAttribute +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +class ListenerAfter implements ListenerAttribute, Multivalue { /** @var string[] */ public array $after = []; diff --git a/src/ListenerBefore.php b/src/ListenerBefore.php index db0db92..3e29fb0 100644 --- a/src/ListenerBefore.php +++ b/src/ListenerBefore.php @@ -5,9 +5,10 @@ namespace Crell\Tukio; use Attribute; +use Crell\AttributeUtils\Multivalue; -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -class ListenerBefore implements ListenerAttribute +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +class ListenerBefore implements ListenerAttribute, Multivalue { /** @var string[] */ public array $before = []; diff --git a/src/ListenerPriority.php b/src/ListenerPriority.php index 6dd8612..17ba90e 100644 --- a/src/ListenerPriority.php +++ b/src/ListenerPriority.php @@ -6,7 +6,7 @@ use Attribute; -#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class ListenerPriority implements ListenerAttribute { public function __construct( diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 8d80a81..d1bedc1 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -57,7 +57,8 @@ public function listenerService( if (class_exists($service)) { $listener = [$service, $method]; /** @var Listener $def */ - $def = $this->getAttributeDefinition($listener); + $def = $this->classAnalyzer->analyze($service, Listener::class); + $def = $def->methods[$method]; $id ??= $def?->id ?? $this->getListenerId($listener); // If any ordering is specified explicitly, that completely overrules any @@ -70,7 +71,6 @@ public function listenerService( return $this->listener($this->makeListenerForService($service, $method), priority: $def->priority, before: $def->before, after: $def->after, id: $id, type: $type); } - $id ??= $service . '-' . $method; return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); } diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 8d71734..7d75a28 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -211,12 +211,12 @@ public function addListenerServiceAfter(string $after, string $service, string $ /** * Registers all listener methods on a service as listeners. * - * A method on the specified class is a listener if: - * - It is public. + * A public method on the specified class is a listener if either of these is true: * - It's name is in the form on*. onUpdate(), onUserLogin(), onHammerTime() will all be registered. - * - It has a Listener/ListenerBefore/ListenerAfter attribute. + * - It has a Listener/ListenerBefore/ListenerAfter/ListenerPriority attribute. * - * The event type the listener is for will be derived from the type declaration in the method signature. + * The event type the listener is for will be derived from the type declaration in the method signature, + * unless overriden by an attribute.. * * @param class-string $class * The class name to be registered as a subscriber. diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index d943cda..19e5704 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -58,7 +58,8 @@ public function listenerService( if (class_exists($service)) { $listener = [$service, $method]; /** @var Listener $def */ - $def = $this->getAttributeDefinition($listener); + $def = $this->classAnalyzer->analyze($service, Listener::class); + $def = $def->methods[$method]; $id ??= $def?->id ?? $this->getListenerId($listener); // If any ordering is specified explicitly, that completely overrules any diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index ce27568..5eb13cf 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -4,6 +4,11 @@ namespace Crell\Tukio; +use Crell\AttributeUtils\Analyzer; +use Crell\AttributeUtils\ClassAnalyzer; +use Crell\AttributeUtils\FuncAnalyzer; +use Crell\AttributeUtils\FunctionAnalyzer; +use Crell\AttributeUtils\MemoryCacheAnalyzer; use Crell\OrderedCollection\MultiOrderedCollection; use Crell\Tukio\Entry\ListenerEntry; use Fig\EventDispatcher\ParameterDeriverTrait; @@ -17,8 +22,10 @@ abstract class ProviderCollector implements OrderedProviderInterface */ protected MultiOrderedCollection $listeners; - public function __construct() - { + public function __construct( + protected readonly FunctionAnalyzer $funcAnalyzer = new FuncAnalyzer(), + protected readonly ClassAnalyzer $classAnalyzer = new MemoryCacheAnalyzer(new Analyzer()), + ) { $this->listeners = new MultiOrderedCollection(); } @@ -86,26 +93,44 @@ public function addListenerServiceAfter(string $after, string $service, string $ public function addSubscriber(string $class, string $service): void { + // First allow manual registration through the proxy object. + // This is deprecated. Please don't use it. $proxy = $this->addSubscribersByProxy($class, $service); - try { - $methods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC); - - $methods = array_filter($methods, static fn(\ReflectionMethod $r) - => !in_array($r->getName(), $proxy->getRegisteredMethods(), true)); + $proxyRegisteredMethods = $proxy->getRegisteredMethods(); - /** @var \ReflectionMethod $rMethod */ - foreach ($methods as $rMethod) { - $this->addSubscriberMethod($rMethod, $class, $service); + try { + // Get all methods on the class, via AttributeUtils to handle reflection and caching. + $methods = $this->classAnalyzer->analyze($class, Listener::class)->methods; + + /** + * @var string $methodName + * @var Listener $def + */ + foreach ($methods as $methodName => $def) { + if (in_array($methodName, $proxyRegisteredMethods, true)) { + // Exclude anything already registered by proxy. + continue; + } + // If there was an attribute-based definition, that takes priority. + if ($def->hasDefinition) { + $this->listenerService($service, $methodName, $def->type, $def->priority, $def->before,$def->after, $def->id); + } elseif (str_starts_with($methodName, 'on') && $def->paramCount === 1) { + // Try to register it iff the method starts with "on" and has only one required parameter. + // (More than one required parameter is guaranteed to fail when invoked.) + if (!$def->type) { + throw InvalidTypeException::fromClassCallable($class, $methodName); + } + $this->listenerService($service, $methodName, type: $def->type, id: $service . '-' . $methodName); + } } } catch (\ReflectionException $e) { throw new \RuntimeException('Type error registering subscriber.', 0, $e); } } - protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class, string $service): void + protected function addSubscriberMethod(string $methodName, Listener $rMethod, string $class, string $service): void { - $methodName = $rMethod->getName(); $params = $rMethod->getParameters(); if (count($params) < 1) { @@ -113,7 +138,11 @@ protected function addSubscriberMethod(\ReflectionMethod $rMethod, string $class return; } - $def = $this->getAttributeForRef($rMethod); + // Definitely wasteful to do this here. + // @todo Refactor. + + + $def = $this->getAttributeDefinition([$rMethod->class, $rMethod->name]); if ($def->id || $def->before || $def->after || $def->priority || str_starts_with($methodName, 'on')) { $paramType = $params[0]->getType(); @@ -158,32 +187,27 @@ protected function addSubscribersByProxy(string $class, string $service): Listen */ protected function getAttributeDefinition(callable|array $listener): Listener { - $ref = null; + if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { + return $this->funcAnalyzer->analyze($listener, Listener::class); + } - if ($this->isFunctionCallable($listener)) { - /** @var string $listener */ - $ref = new \ReflectionFunction($listener); - // @phpstan-ignore-next-line - } elseif ($this->isClassCallable($listener)) { - // PHPStan says you cannot use array destructuring on a callable, but you can - // if you know that it's an array (which in context we do). - // @phpstan-ignore-next-line - [$class, $method] = $listener; - $ref = (new \ReflectionClass($class))->getMethod($method); - // @phpstan-ignore-next-line - } elseif ($this->isObjectCallable($listener)) { - // PHPStan says you cannot use array destructuring on a callable, but you can - // if you know that it's an array (which in context we do). - // @phpstan-ignore-next-line - [$class, $method] = $listener; - $ref = (new \ReflectionObject($class))->getMethod($method); + if ($this->isObjectCallable($listener)) { + /** @var array $listener */ + [$object, $method] = $listener; + + $def = $this->classAnalyzer->analyze($object::class, Listener::class); + return $def->methods[$method]; } - if (!$ref) { - return new Listener(); + if ($this->isClassCallable($listener)) { + /** @var array $listener */ + [$class, $method] = $listener; + + $def = $this->classAnalyzer->analyze($class, Listener::class); + return $def->staticMethods[$method]; } - return $this->getAttributeForRef($ref); + return new Listener(); } protected function getAttributeForRef(\Reflector $ref): Listener @@ -323,10 +347,11 @@ protected function isFunctionCallable(callable|array $callable): bool /** * Determines if a callable represents a method on an object. * + * @param callable|array{0: string, 1: string} $callable * @return bool * True if the callable represents a method object, false otherwise. */ - protected function isObjectCallable(callable $callable): bool + protected function isObjectCallable(callable|array $callable): bool { return is_array($callable) && is_object($callable[0]); } @@ -334,10 +359,11 @@ protected function isObjectCallable(callable $callable): bool /** * Determines if a callable represents a closure/anonymous function. * + * @param callable|array{0: string, 1: string} $callable * @return bool * True if the callable represents a closure object, false otherwise. */ - protected function isClosureCallable(callable $callable): bool + protected function isClosureCallable(callable|array $callable): bool { return $callable instanceof \Closure; } diff --git a/tests/CompiledListenerProviderAttributeTest.php b/tests/CompiledListenerProviderAttributeTest.php index bbaacd3..1da76c7 100644 --- a/tests/CompiledListenerProviderAttributeTest.php +++ b/tests/CompiledListenerProviderAttributeTest.php @@ -33,23 +33,6 @@ function atNoListen(EventOne $event): void throw new \Exception('This should not be called'); } -class AtListen -{ - #[Listener] - public static function listen(CollectingEvent $event): void - { - $event->add('C'); - } -} - -class AtListenService -{ - public static function listen(CollectingEvent $event): void - { - $event->add('D'); - } -} - class CompiledListenerProviderAttributeTest extends TestCase { use MakeCompiledProviderTrait; @@ -63,13 +46,13 @@ public function compiled_provider_triggers_in_order(): void $builder = new ProviderBuilder(); $container = new MockContainer(); - $container->addService('D', new AtListenService()); + $container->addService('D', new Listeners\AtListenService()); $ns = "\\Crell\\Tukio"; $builder->addListener("{$ns}\\atListenerB"); $builder->addListener("{$ns}\\atListenerA"); - $builder->addListener([AtListen::class, 'listen']); + $builder->addListener([Listeners\AtListen::class, 'listen']); $builder->addListener("{$ns}\\atNoListen"); $provider = $this->makeProvider($builder, $container, $class, $namespace); @@ -99,7 +82,7 @@ public function add_subscriber(): void $subscriber = new MockAttributedSubscriber(); $container->addService('subscriber', $subscriber); - $builder->addSubscriber(MockSubscriber::class, 'subscriber'); + $builder->addSubscriber(MockAttributedSubscriber::class, 'subscriber'); $provider = $this->makeProvider($builder, $container, $class, $namespace); diff --git a/tests/Listeners/AtListen.php b/tests/Listeners/AtListen.php new file mode 100644 index 0000000..6d958cc --- /dev/null +++ b/tests/Listeners/AtListen.php @@ -0,0 +1,15 @@ +add('C'); + } +} diff --git a/tests/Listeners/AtListenService.php b/tests/Listeners/AtListenService.php new file mode 100644 index 0000000..0b0f23a --- /dev/null +++ b/tests/Listeners/AtListenService.php @@ -0,0 +1,13 @@ +add('D'); + } +} diff --git a/tests/Listeners/MockMalformedSubscriber.php b/tests/Listeners/MockMalformedSubscriber.php index cf277d7..a02f669 100644 --- a/tests/Listeners/MockMalformedSubscriber.php +++ b/tests/Listeners/MockMalformedSubscriber.php @@ -16,6 +16,7 @@ public function onA(CollectingEvent $event): void { $event->add('A'); } + /** * This function should have automatic registration attempted, and fail due to missing a type. */ @@ -24,6 +25,7 @@ public function onNone($event): void { $event->add('A'); } + /** * This function should have manual registration attempted, and fail due to missing a type. */ @@ -39,12 +41,14 @@ public static function registerListenersDirect(ListenerProxy $proxy): void // Should fail and throw an exception: $proxy->addListener('abnormalNameWithoutType'); } + public static function registerListenersBefore(ListenerProxy $proxy): void { $a = $proxy->addListener('onA'); // Should fail and throw an exception: $proxy->addListenerBefore($a, 'abnormalNameWithoutType'); } + public static function registerListenersAfter(ListenerProxy $proxy): void { $a = $proxy->addListener('onA'); diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 611ef74..b198de5 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -176,11 +176,11 @@ public function malformed_subscriber_automatic_fails(): void $subscriber = new MockMalformedSubscriber(); - $container->addService('subscriber', $subscriber); + $container->addService(MockMalformedSubscriber::class, $subscriber); $p = new OrderedListenerProvider($container); - $p->addSubscriber(MockMalformedSubscriber::class, 'subscriber'); + $p->addSubscriber(MockMalformedSubscriber::class, MockMalformedSubscriber::class); } #[Test] From fe4dd8f04633cafe618cd2983bed871a42d3ece9 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:01:48 -0600 Subject: [PATCH 58/77] Remove dead code. --- src/ProviderCollector.php | 64 --------------------------------------- 1 file changed, 64 deletions(-) diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 5eb13cf..bc0f0bc 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -129,44 +129,6 @@ public function addSubscriber(string $class, string $service): void } } - protected function addSubscriberMethod(string $methodName, Listener $rMethod, string $class, string $service): void - { - $params = $rMethod->getParameters(); - - if (count($params) < 1) { - // Skip this method, as it doesn't take arguments. - return; - } - - // Definitely wasteful to do this here. - // @todo Refactor. - - - $def = $this->getAttributeDefinition([$rMethod->class, $rMethod->name]); - - if ($def->id || $def->before || $def->after || $def->priority || str_starts_with($methodName, 'on')) { - $paramType = $params[0]->getType(); - - $id = $def->id ?? $service . '-' . $methodName; - // getName() is not a documented part of the Reflection API, but it's always there. - // @phpstan-ignore-next-line - $type = $def->type ?? $paramType?->getName() ?? throw InvalidTypeException::fromClassCallable($class, $methodName); - - $this->listenerService($service, $methodName, $type, $def->priority, $def->before,$def->after, $id); - } - } - - /** - * @return array - */ - protected function findAttributesOnMethod(\ReflectionMethod $rMethod): array - { - $attributes = array_map(static fn (\ReflectionAttribute $attrib): object - => $attrib->newInstance(), $rMethod->getAttributes(Listener::class, \ReflectionAttribute::IS_INSTANCEOF)); - - return $attributes; - } - /** * @param class-string $class */ @@ -210,32 +172,6 @@ protected function getAttributeDefinition(callable|array $listener): Listener return new Listener(); } - protected function getAttributeForRef(\Reflector $ref): Listener - { - // All this logic is very similar to AttributeUtils Sub-Attributes. - // Maybe AU can be improved to make sub-attributes accessible outside - // the analyzer? - - /** @var Listener $def */ - $def = $this->getAttributes(Listener::class, $ref)[0] ?? new Listener(); - - /** @var ListenerBefore[] $beforeAttribs */ - $beforeAttribs = $this->getAttributes(ListenerBefore::class, $ref); - $def->absorbBefore($beforeAttribs); - - /** @var ListenerAfter[] $afterAttribs */ - $afterAttribs = $this->getAttributes(ListenerAfter::class, $ref); - $def->absorbAfter($afterAttribs); - - /** @var ListenerPriority|null $priorityAttrib */ - $priorityAttrib = $this->getAttributes(ListenerPriority::class, $ref)[0] ?? null; - if ($priorityAttrib) { - $def->absorbPriority($priorityAttrib); - } - - return $def; - } - /** * @param class-string $attribute * @param \Reflector $ref From b1e36f852b870e56a534d801a6703e031c756414 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:09:20 -0600 Subject: [PATCH 59/77] Make the separate service name optional when registering a subscriber. --- src/OrderedProviderInterface.php | 6 +++--- src/ProviderCollector.php | 4 +++- tests/OrderedListenerProviderServiceTest.php | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index 7d75a28..d92331f 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -220,8 +220,8 @@ public function addListenerServiceAfter(string $after, string $service, string $ * * @param class-string $class * The class name to be registered as a subscriber. - * @param string $service - * The name of a service in the container. + * @param null|string $service + * The name of a service in the container. If not specified, it's assumed to be the same as the class. */ - public function addSubscriber(string $class, string $service): void; + public function addSubscriber(string $class, ?string $service = null): void; } diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index bc0f0bc..288dadd 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -91,8 +91,10 @@ public function addListenerServiceAfter(string $after, string $service, string $ return $this->listenerService($service, $method, $type, after: [$after], id: $id); } - public function addSubscriber(string $class, string $service): void + public function addSubscriber(string $class, ?string $service = null): void { + $service ??= $class; + // First allow manual registration through the proxy object. // This is deprecated. Please don't use it. $proxy = $this->addSubscribersByProxy($class, $service); diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index b198de5..c01cd6d 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -180,7 +180,7 @@ public function malformed_subscriber_automatic_fails(): void $p = new OrderedListenerProvider($container); - $p->addSubscriber(MockMalformedSubscriber::class, MockMalformedSubscriber::class); + $p->addSubscriber(MockMalformedSubscriber::class); } #[Test] From 62ad14267dc7c3eabb80f858fd22865caabfcf6a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:35:39 -0600 Subject: [PATCH 60/77] PHPStan fixes. --- src/Listener.php | 9 +++- src/ListenerProxy.php | 2 +- src/ProviderCollector.php | 51 +++++++++++++++----- tests/OrderedListenerProviderServiceTest.php | 2 +- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index e08cb38..36ae5c3 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -59,7 +59,7 @@ public function __construct( public ?string $type = null, ) { if ($id || $this->type) { - $this->hasDefinition ??= true; + $this->hasDefinition = true; } } @@ -68,7 +68,9 @@ public function fromReflection(\ReflectionMethod $subject): void $this->paramCount = $subject->getNumberOfRequiredParameters(); if ($this->paramCount === 1) { $params = $subject->getParameters(); - $this->type ??= $params[0]?->getType()?->getName(); + // getName() isn't part of the interface, but is present. PHP bug. + // @phpstan-ignore-next-line + $this->type ??= $params[0]->getType()?->getName(); } } @@ -92,6 +94,9 @@ public function methodAttribute(): string return __CLASS__; } + /** + * @param array $methods + */ public function setStaticMethods(array $methods): void { $this->staticMethods = $methods; diff --git a/src/ListenerProxy.php b/src/ListenerProxy.php index 8541aea..24e9091 100644 --- a/src/ListenerProxy.php +++ b/src/ListenerProxy.php @@ -123,7 +123,7 @@ protected function getServiceMethodType(string $methodName): string { try { // We don't have a real object here, so we cannot use first-class-closures. - // PHPStan complains that an aray is not a callable, even though it is, because PHP. + // PHPStan complains that an array is not a callable, even though it is, because PHP. // @phpstan-ignore-next-line $type = $this->getParameterType([$this->serviceClass, $methodName]); } catch (\InvalidArgumentException $exception) { diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 288dadd..1fbbd7c 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -147,24 +147,26 @@ protected function addSubscribersByProxy(string $class, string $service): Listen } /** - * @param callable|array{0: string, 1: string} $listener + * @param callable|array{0: class-string, 1: string}|array{0: object, 1: string} $listener */ protected function getAttributeDefinition(callable|array $listener): Listener { if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { + /** @var \Closure|string $listener */ return $this->funcAnalyzer->analyze($listener, Listener::class); } if ($this->isObjectCallable($listener)) { - /** @var array $listener */ + /** @var array{0: object, 1: string} $listener */ [$object, $method] = $listener; $def = $this->classAnalyzer->analyze($object::class, Listener::class); return $def->methods[$method]; } + /** @var array{0: class-string, 1: string} $listener */ if ($this->isClassCallable($listener)) { - /** @var array $listener */ + /** @var array{0: class-string, 1: string} $listener */ [$class, $method] = $listener; $def = $this->classAnalyzer->analyze($class, Listener::class); @@ -241,7 +243,7 @@ protected function getType(callable $listener): string * generate a random ID if necessary. It will also handle duplicates * for us. This method is just a suggestion. * - * @param callable|array{0: string, 1: string} $listener + * @param callable|array{0: class-string, 1: string}|array{0: object, 1: string} $listener * The listener for which to derive an ID. * * @return string|null @@ -251,17 +253,19 @@ protected function getListenerId(callable|array $listener): ?string { if ($this->isFunctionCallable($listener)) { // Function callables are strings, so use that directly. - // @phpstan-ignore-next-line - return (string)$listener; + /** @var string $listener */ + return $listener; } - // @phpstan-ignore-next-line + + if ($this->isObjectCallable($listener)) { + /** @var array{0: object, 1: string} $listener */ + return get_class($listener[0]) . '::' . $listener[1]; + } + if ($this->isClassCallable($listener)) { /** @var array{0: class-string, 1: string} $listener */ return $listener[0] . '::' . $listener[1]; } - if (is_array($listener) && is_object($listener[0])) { - return get_class($listener[0]) . '::' . $listener[1]; - } // Anything else we can't derive an ID for logically. return null; @@ -272,7 +276,7 @@ protected function getListenerId(callable|array $listener): ?string * * Or at least a reasonable approximation, since a function name may not be defined yet. * - * @param callable|array{0: string, 1: string} $callable + * @param callable|array $callable * @return bool * True if the callable represents a function, false otherwise. */ @@ -285,7 +289,7 @@ protected function isFunctionCallable(callable|array $callable): bool /** * Determines if a callable represents a method on an object. * - * @param callable|array{0: string, 1: string} $callable + * @param callable|array $callable * @return bool * True if the callable represents a method object, false otherwise. */ @@ -297,7 +301,7 @@ protected function isObjectCallable(callable|array $callable): bool /** * Determines if a callable represents a closure/anonymous function. * - * @param callable|array{0: string, 1: string} $callable + * @param callable|array $callable * @return bool * True if the callable represents a closure object, false otherwise. */ @@ -306,5 +310,26 @@ protected function isClosureCallable(callable|array $callable): bool return $callable instanceof \Closure; } + /** + * Determines if a callable represents a static class method. + * + * The parameter here is untyped so that this method may be called with an + * array that represents a class name and a non-static method. The routine + * to determine the parameter type is identical to a static method, but such + * an array is still not technically callable. Omitting the parameter type here + * allows us to use this method to handle both cases. + * + * This method must therefore be called first above, as the array is not actually + * an `is_callable()` and will fail `Closure::fromCallable()`. Because PHP. + * + * @param callable|array $callable + * @return bool + * True if the callable represents a static method, false otherwise. + */ + protected function isClassCallable($callable): bool + { + return is_array($callable) && is_string($callable[0]) && class_exists($callable[0]); + } + abstract protected function getListenerEntry(callable $listener, string $type): ListenerEntry; } diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index c01cd6d..f5e3c4d 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -287,7 +287,7 @@ public function rejects_missing_auto_detected_service(): void $provider = new OrderedListenerProvider($container); - /** @phpstan-ignore-next-line */ + // @phpstan-ignore-next-line $provider->listenerService(DoesNotExist::class); } From 211c22744475518681d586d4ffd43da16f209c4a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:53:23 -0600 Subject: [PATCH 61/77] Skip routing everything through listener() to avoid double-deriving everything. --- src/OrderedListenerProvider.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index d1bedc1..830c38f 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -68,11 +68,23 @@ public function listenerService( $def->before = $before; $def->after = $after; } - return $this->listener($this->makeListenerForService($service, $method), priority: $def->priority, before: $def->before, after: $def->after, id: $id, type: $type); + return $this->listeners->add( + item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), + id: $id, + priority: $def->priority, + before: $def->before, + after: $def->after + ); } $id ??= $service . '-' . $method; - return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type); + return $this->listeners->add( + item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), + id: $id, + priority: $priority, + before: $before, + after: $after, + ); } /** From ff1110a242d5e6350664bedd309768ff9375f1ff Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 18:55:47 -0600 Subject: [PATCH 62/77] Remove dead code. --- src/ProviderCollector.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 1fbbd7c..dc525fd 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -176,20 +176,6 @@ protected function getAttributeDefinition(callable|array $listener): Listener return new Listener(); } - /** - * @param class-string $attribute - * @param \Reflector $ref - * @return array - */ - protected function getAttributes(string $attribute, \Reflector $ref): array - { - // The Reflector interface doesn't have getAttributes() defined, but - // it's always there. PHP bug. - // @phpstan-ignore-next-line - $attribs = $ref->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF); - return array_map(fn(\ReflectionAttribute $attrib) => $attrib->newInstance(), $attribs); - } - protected function deriveMethod(string $service): string { if (!class_exists($service)) { From 23946aa7b2360534e1d839b7f2a622df1e848edc Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 19:00:56 -0600 Subject: [PATCH 63/77] Be consistent in ID generation. --- src/OrderedListenerProvider.php | 2 +- tests/OrderedListenerProviderIdTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 830c38f..35fd629 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -77,7 +77,7 @@ public function listenerService( ); } - $id ??= $service . '-' . $method; + $id ??= $service . '::' . $method; return $this->listeners->add( item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), id: $id, diff --git a/tests/OrderedListenerProviderIdTest.php b/tests/OrderedListenerProviderIdTest.php index 3a39500..d5d772f 100644 --- a/tests/OrderedListenerProviderIdTest.php +++ b/tests/OrderedListenerProviderIdTest.php @@ -137,14 +137,14 @@ public function listen(CollectingEvent $event): void $p = new OrderedListenerProvider($container); $idA = $p->addListenerService('A', 'listen', CollectingEvent::class, -4); - $p->addListenerServiceAfter('A-listen', 'B', 'listen', CollectingEvent::class); + $p->addListenerServiceAfter('A::listen', 'B', 'listen', CollectingEvent::class); $event = new CollectingEvent(); foreach ($p->getListenersForEvent($event) as $listener) { $listener($event); } - self::assertEquals('A-listen', $idA); + self::assertEquals('A::listen', $idA); self::assertEquals('AB', implode($event->result())); } From cb4ec6f351f7782906e70e7679bbc80a1968e9fa Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 19:05:24 -0600 Subject: [PATCH 64/77] Minor refactor for performance and cleanliness. --- src/OrderedListenerProvider.php | 11 +++-------- src/ProviderBuilder.php | 12 +++--------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/OrderedListenerProvider.php b/src/OrderedListenerProvider.php index 35fd629..722f42c 100644 --- a/src/OrderedListenerProvider.php +++ b/src/OrderedListenerProvider.php @@ -52,22 +52,17 @@ public function listenerService( $type = $this->getParameterType([$service, $method]); } + $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); + // In the special case that the service is the class name, we can // leverage attributes. - if (class_exists($service)) { + if (!$orderSpecified && class_exists($service)) { $listener = [$service, $method]; /** @var Listener $def */ $def = $this->classAnalyzer->analyze($service, Listener::class); $def = $def->methods[$method]; $id ??= $def?->id ?? $this->getListenerId($listener); - // If any ordering is specified explicitly, that completely overrules any - // attributes. - if (!is_null($priority) || $before || $after) { - $def->priority = $priority; - $def->before = $before; - $def->after = $after; - } return $this->listeners->add( item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), id: $id, diff --git a/src/ProviderBuilder.php b/src/ProviderBuilder.php index 19e5704..2e03110 100644 --- a/src/ProviderBuilder.php +++ b/src/ProviderBuilder.php @@ -53,23 +53,17 @@ public function listenerService( $type = $this->getParameterType([$service, $method]); } + $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); + // In the special case that the service is the class name, we can // leverage attributes. - if (class_exists($service)) { + if (!$orderSpecified && class_exists($service)) { $listener = [$service, $method]; /** @var Listener $def */ $def = $this->classAnalyzer->analyze($service, Listener::class); $def = $def->methods[$method]; $id ??= $def?->id ?? $this->getListenerId($listener); - // If any ordering is specified explicitly, that completely overrules any - // attributes. - if (!is_null($priority) || $before || $after) { - $def->priority = $priority; - $def->before = $before; - $def->after = $after; - } - $entry = new ListenerServiceEntry($service, $method, $type); return $this->listeners->add($entry, $id, priority: $def->priority, before: $def->before, after: $def->after); } From 56b5674701469594598c9c6b2f9584fd61a40f6d Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 19:11:05 -0600 Subject: [PATCH 65/77] Refactor to avoid parsing for an attribute if we can avoid it. --- src/ProviderCollector.php | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index dc525fd..4211d52 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -37,17 +37,21 @@ public function listener( ?string $id = null, ?string $type = null ): string { - /** @var Listener $def */ - $def = $this->getAttributeDefinition($listener); - $id ??= $def?->id ?? $this->getListenerId($listener); - $type ??= $def?->type ?? $this->getType($listener); - - // If any ordering is specified explicitly, that completely overrules any - // attributes. - if (!is_null($priority) || $before || $after) { - $def->priority = $priority; - $def->before = $before; - $def->after = $after; + $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); + + if (!$orderSpecified || !$type || !$id) { + /** @var Listener $def */ + $def = $this->getAttributeDefinition($listener); + $id ??= $def?->id ?? $this->getListenerId($listener); + $type ??= $def?->type ?? $this->getType($listener); + + // If any ordering is specified explicitly, that completely overrules any + // attributes. + if (!$orderSpecified) { + $priority = $def->priority; + $before = $def->before; + $after = $def->after; + } } $entry = $this->getListenerEntry($listener, $type); @@ -55,9 +59,9 @@ public function listener( return $this->listeners->add( item: $entry, id: $id, - priority: $def->priority, - before: $def->before, - after: $def->after + priority: $priority, + before: $before, + after: $after ); } From 377f51d8f5560e7dc97cb75a8cdb1b3324c64a2a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 9 Jan 2024 19:23:31 -0600 Subject: [PATCH 66/77] Add tests to confirm class-level attributes get propagated down to methods. --- tests/CompiledListenerProviderTest.php | 2 -- .../InvokableListenerClassAttribute.php | 15 ++++++++++++ tests/OrderedListenerProviderServiceTest.php | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/Listeners/InvokableListenerClassAttribute.php diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 96a875b..3b2bf78 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -245,8 +245,6 @@ public function optimize_event_anonymous_class(): void self::assertEquals('BACD', implode($event->result())); } - - #[Test, DataProvider('detection_class_examples')] public function detects_invoke_method_and_type(string $class): void { diff --git a/tests/Listeners/InvokableListenerClassAttribute.php b/tests/Listeners/InvokableListenerClassAttribute.php new file mode 100644 index 0000000..1d952a8 --- /dev/null +++ b/tests/Listeners/InvokableListenerClassAttribute.php @@ -0,0 +1,15 @@ +add(static::class); + } +} diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index f5e3c4d..1e0287c 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -6,6 +6,7 @@ use Crell\Tukio\Events\CollectingEvent; use Crell\Tukio\Fakes\MockContainer; +use Crell\Tukio\Listeners\InvokableListenerClassAttribute; use Crell\Tukio\Listeners\MockMalformedSubscriber; use Crell\Tukio\Listeners\MockSubscriber; use PHPUnit\Framework\Attributes\DataProvider; @@ -266,6 +267,29 @@ public static function detection_class_examples(): iterable ]; } + #[Test] + public function detects_invoke_method_and_type_with_class_attribute(): void + { + $container = new MockContainer(); + + $container->addService(InvokableListenerClassAttribute::class, new InvokableListenerClassAttribute()); + + $provider = new OrderedListenerProvider($container); + + $provider->listenerService(InvokableListenerClassAttribute::class); + $provider->listener(fn(CollectingEvent $event) => $event->add('A'), priority: 10); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $results = $event->result(); + self::assertEquals('A', $results[0]); + self::assertEquals(InvokableListenerClassAttribute::class, $results[1]); + } + #[Test] public function rejects_multi_method_class_without_invoke(): void { From 2c5827b78b5388e2a999a6a31558026c0181635b Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 18 Feb 2024 11:59:22 -0600 Subject: [PATCH 67/77] Use a cached function analyzer by default. --- src/ProviderCollector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 4211d52..561892b 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -9,6 +9,7 @@ use Crell\AttributeUtils\FuncAnalyzer; use Crell\AttributeUtils\FunctionAnalyzer; use Crell\AttributeUtils\MemoryCacheAnalyzer; +use Crell\AttributeUtils\MemoryCacheFunctionAnalyzer; use Crell\OrderedCollection\MultiOrderedCollection; use Crell\Tukio\Entry\ListenerEntry; use Fig\EventDispatcher\ParameterDeriverTrait; @@ -23,7 +24,7 @@ abstract class ProviderCollector implements OrderedProviderInterface protected MultiOrderedCollection $listeners; public function __construct( - protected readonly FunctionAnalyzer $funcAnalyzer = new FuncAnalyzer(), + protected readonly FunctionAnalyzer $funcAnalyzer = new MemoryCacheFunctionAnalyzer(new FuncAnalyzer()), protected readonly ClassAnalyzer $classAnalyzer = new MemoryCacheAnalyzer(new Analyzer()), ) { $this->listeners = new MultiOrderedCollection(); From 2b14d4985ffffac3aa6071be8bb610d98d2501e2 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 24 Feb 2024 10:42:31 -0600 Subject: [PATCH 68/77] Use stable release of AttributeUtils. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c9cbd22..76aedb5 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": "~8.1", - "crell/attributeutils": "dev-func-analyzer", + "crell/attributeutils": "^1.1", "crell/ordered-collection": "v2.x-dev", "fig/event-dispatcher-util": "^1.3", "psr/container": "^1.0 || ^2.0", From e0223e49fd439ef5c24ba9a4e65db256d681b893 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 18:30:35 -0500 Subject: [PATCH 69/77] Mark older registration mechanisms as deprecated. --- src/OrderedProviderInterface.php | 6 ++++++ src/SubscriberInterface.php | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/OrderedProviderInterface.php b/src/OrderedProviderInterface.php index d92331f..d5dd3a1 100644 --- a/src/OrderedProviderInterface.php +++ b/src/OrderedProviderInterface.php @@ -76,6 +76,7 @@ public function listenerService( * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide * the priority, id, or type. Values specified in the method call take priority over the attribute. * + * @deprecated * @param callable $listener * The listener to register. * @param ?int $priority @@ -100,6 +101,7 @@ public function addListener(callable $listener, ?int $priority = null, ?string $ * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide * the id or type. The $before parameter specified here will always be used and the type of attribute ignored. * + * @deprecated * @param string $before * The ID of an existing listener. * @param callable $listener @@ -124,6 +126,7 @@ public function addListenerBefore(string $before, callable $listener, ?string $i * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide * the id or type. The $after parameter specified here will always be used and the type of attribute ignored. * + * @deprecated * @param string $after * The ID of an existing listener. * @param callable $listener @@ -144,6 +147,7 @@ public function addListenerAfter(string $after, callable $listener, ?string $id * * This method does not support attributes, as the class name is unknown at registration. * + * @deprecated * @param string $service * The name of a service on which this listener lives. * @param string $method @@ -168,6 +172,7 @@ public function addListenerService(string $service, string $method, string $type * * This method does not support attributes, as the class name is unknown at registration. * + * @deprecated * @param string $before * The ID of an existing listener. * @param string $service @@ -192,6 +197,7 @@ public function addListenerServiceBefore(string $before, string $service, string * * This method does not support attributes, as the class name is unknown at registration. * + * @deprecated * @param string $after * The ID of an existing listener. * @param string $service diff --git a/src/SubscriberInterface.php b/src/SubscriberInterface.php index 99c2bd7..11e7bd7 100644 --- a/src/SubscriberInterface.php +++ b/src/SubscriberInterface.php @@ -4,6 +4,9 @@ namespace Crell\Tukio; +/** + * @deprecated + */ interface SubscriberInterface { public static function registerListeners(ListenerProxy $proxy): void; From fa63133dedde5165119ba52bbeaa518ecca7949a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:00:10 -0500 Subject: [PATCH 70/77] Fix bug in inheriting attribute information. --- src/Listener.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Listener.php b/src/Listener.php index 36ae5c3..6ad67e3 100644 --- a/src/Listener.php +++ b/src/Listener.php @@ -123,8 +123,8 @@ public function fromClassAttribute(object $class): void $this->id ??= $class->id; $this->type ??= $class->type; $this->priority ??= $class->priority; - $this->before ??= $class->before; - $this->after ??= $class->after; + $this->before = [...$this->before, ...$class->before]; + $this->after = [...$this->after, ...$class->after]; } public function subAttributes(): array From 89d981bb08b12d2d00efdb25b028d0e21fb69107 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:00:52 -0500 Subject: [PATCH 71/77] If the method of a listener is __invoke, use the class name as the ID. --- src/ProviderCollector.php | 3 ++ tests/CompiledListenerProviderTest.php | 28 +++++++++++++++++++ .../Listeners/InvokableListenerClassNoId.php | 15 ++++++++++ .../InvokableListenerClassNoIdBefore.php | 16 +++++++++++ tests/OrderedListenerProviderServiceTest.php | 14 ++++++++++ 5 files changed, 76 insertions(+) create mode 100644 tests/Listeners/InvokableListenerClassNoId.php create mode 100644 tests/Listeners/InvokableListenerClassNoIdBefore.php diff --git a/src/ProviderCollector.php b/src/ProviderCollector.php index 561892b..ab15c5d 100644 --- a/src/ProviderCollector.php +++ b/src/ProviderCollector.php @@ -255,6 +255,9 @@ protected function getListenerId(callable|array $listener): ?string if ($this->isClassCallable($listener)) { /** @var array{0: class-string, 1: string} $listener */ + if ($listener[1] === '__invoke') { + return $listener[0]; + } return $listener[0] . '::' . $listener[1]; } diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 3b2bf78..4f0c8fe 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -11,6 +11,9 @@ use Crell\Tukio\Listeners\CompoundListener; use Crell\Tukio\Listeners\InvalidListener; use Crell\Tukio\Listeners\InvokableListener; +use Crell\Tukio\Listeners\InvokableListenerClassAttribute; +use Crell\Tukio\Listeners\InvokableListenerClassNoId; +use Crell\Tukio\Listeners\InvokableListenerClassNoIdBefore; use Crell\Tukio\Listeners\MockSubscriber; use Crell\Tukio\Listeners\TestAttributedListeners; use PHPUnit\Framework\Attributes\DataProvider; @@ -323,4 +326,29 @@ public function add_attribute_based_service_methods(): void self::assertEquals('DC', implode($event->result())); } + + #[Test] + public function detects_invoke_method_and_gives_class_id_by_default(): void + { + $builder = new ProviderBuilder(); + $container = new MockContainer(); + + $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId()); + $container->addService(InvokableListenerClassNoIdBefore::class, new InvokableListenerClassNoIdBefore()); + + $builder->listenerService(InvokableListenerClassNoId::class); + $builder->listenerService(InvokableListenerClassNoIdBefore::class); + + $provider = $this->makeAnonymousProvider($builder, $container); + + $event = new CollectingEvent(); + + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + + $results = $event->result(); + self::assertEquals(InvokableListenerClassNoIdBefore::class, $results[0]); + self::assertEquals(InvokableListenerClassNoId::class, $results[1]); + } } diff --git a/tests/Listeners/InvokableListenerClassNoId.php b/tests/Listeners/InvokableListenerClassNoId.php new file mode 100644 index 0000000..b10b855 --- /dev/null +++ b/tests/Listeners/InvokableListenerClassNoId.php @@ -0,0 +1,15 @@ +add(static::class); + } +} diff --git a/tests/Listeners/InvokableListenerClassNoIdBefore.php b/tests/Listeners/InvokableListenerClassNoIdBefore.php new file mode 100644 index 0000000..3da2544 --- /dev/null +++ b/tests/Listeners/InvokableListenerClassNoIdBefore.php @@ -0,0 +1,16 @@ +add(static::class); + } +} diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index 1e0287c..cdb5a85 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -7,6 +7,7 @@ use Crell\Tukio\Events\CollectingEvent; use Crell\Tukio\Fakes\MockContainer; use Crell\Tukio\Listeners\InvokableListenerClassAttribute; +use Crell\Tukio\Listeners\InvokableListenerClassNoId; use Crell\Tukio\Listeners\MockMalformedSubscriber; use Crell\Tukio\Listeners\MockSubscriber; use PHPUnit\Framework\Attributes\DataProvider; @@ -290,6 +291,19 @@ public function detects_invoke_method_and_type_with_class_attribute(): void self::assertEquals(InvokableListenerClassAttribute::class, $results[1]); } + #[Test] + public function detects_invoke_method_and_gives_class_id_by_default(): void + { + $container = new MockContainer(); + $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId()); + + $provider = new OrderedListenerProvider($container); + + $id = $provider->listenerService(InvokableListenerClassNoId::class); + + self::assertEquals(InvokableListenerClassNoId::class, $id); + } + #[Test] public function rejects_multi_method_class_without_invoke(): void { From 2585916ed4d92f9651a926b7d405842f5f9f2a7f Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:07:52 -0500 Subject: [PATCH 72/77] Add a SECURITY.md file for GitHub. --- SECURITY.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ec4301b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Brand Promise + +Perfect security is not an achievable goal, but it is a goal to strive for nonetheless. To that end, we welcome responsible security reports from both users and external security researchers. + +# Scope + +If you believe you've found a security issue in software that is maintained in this repository, we encourage you to notify us. + +| Version | In scope | Source code | +| ------- | -------- |--------------------------| +| latest | ✅ | https://github.com/Crell/Tukio | + +Only the latest stable release of this library is supported. In general, bug and security fixes will not be backported unless there is a substantial imminent threat to users in not doing so. + +# How to Submit a Report + +To submit a vulnerability report, please contact us through [GitHub](https://github.com/Crell/Tukio/security). Your submission will be reviewed as soon as feasible, but as this is a volunteer project we cannot guarantee a response time. + +# Safe Harbor + +We support safe harbor for security researchers who: + +* Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services. +* Only interact with accounts you own or with explicit permission of the account holder. If you do encounter Personally Identifiable Information (PII) contact us immediately, do not proceed with access, and immediately purge any local information. +* Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party. + +We will consider activities conducted consistent with this policy to constitute "authorized" conduct and will not pursue civil action or initiate a complaint to law enforcement. We will help to the extent we can if legal action is initiated by a third party against you. + +Please submit a report to us before engaging in conduct that may be inconsistent with or unaddressed by this policy. + +# Preferences + +* Please provide detailed reports with reproducible steps and a clearly defined impact. +* Include the version number of the vulnerable package in your report. +* Providing a suggested fix is welcome, but not required, and we may choose to implement our own, based on your submitted fix or not. +* This is a volunteer project. We will make every effort to respond to security reports in a timely manner, but that may be a week or two on the first contact. + From 29df26b71da486c2f16c28d8b672ce73fc86dc4d Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:21:21 -0500 Subject: [PATCH 73/77] Update README file for all the new goodies in version 2. --- README.md | 368 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 239 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 61437e8..25e3a88 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Which Provider or Providers you use depends on your use case. All of them are v As the name implies, `OrderedListenerProvider` is all about ordering. Users can explicitly register Listeners on it that will be matched against an Event based on its type. -If no order is specified, then the order that Listeners will be returned is undefined (although in practice order-added is what it will most likely be). That makes the degenerate case super-easy: +If no order is specified, then the order that Listeners will be returned is undefined, and in practice users should expect the order to be stable, but not predictable. That makes the degenerate case super-easy: ```php use Crell\Tukio\OrderedListenerProvider; @@ -92,34 +92,34 @@ function handleStuff(StuffHappened $stuff) { ... } $provider = new OrderedListenerProvider(); -$provider->addListener(function(SpecificStuffHappened) { +$provider->listener(function(SpecificStuffHappened) { // ... }); -$provider->addListener('handleStuff'); +$provider->listener('handleStuff'); ``` That adds two Listeners to the Provider; one anonymous function and one named function. The anonymous function will be called for any `SpecificStuffHappened` event. The named function will be called for any `StuffHappened` *or* `SpecificStuffHappened` event. And the user doesn't really care which one happens first (which is the typical case). #### Ordering listeners -However, the user can also be picky about the order in which Listeners will fire. The most common approach for that today is via an integral "priority", and Tukio offers that: +However, the user can also be picky about the order in which Listeners will fire. Tukio supports two ordering mechanisms: Priority order, and Topological sorting (before/after markers). Internally, Tukio will convert priority ordering into topological ordering. ```php use Crell\Tukio\OrderedListenerProvider; $provider = new OrderedListenerProvider(); -$provider->addListener(function(SpecificStuffHappened) { +$provider->listener(function(SpecificStuffHappened) { // ... -}, 10); +}, priority: 10); -$provider->addListener('handleStuff', 20); +$provider->addListener('handleStuff', priority: 20); ``` Now, the named function Listener will get called before the anonymous function does. (Higher priority number comes first, and negative numbers are totally legal.) If two listeners have the same priority then their order relative to each other is undefined. -Sometimes, though, you may not know the priority of another Listener, but it's important your Listener happen before or after it. For that we need to add a new concept: IDs. Every Listener has an ID, which can be provided when the Listener is added or will be auto-generated if not. The auto-generated value is predictable (the name of a function, the class-and-method of an object method, etc.), so in most cases it's not necessary to read the return value of `addListener()` although that is slightly more robust. +Sometimes, though, you may not know the priority of another Listener, but it's important your Listener happen before or after it. For that we need to add a new concept: IDs. Every Listener has an ID, which can be provided when the Listener is added or will be auto-generated if not. The auto-generated value is predictable (the name of a function, the class-and-method of an object method, etc.), so in most cases it's not necessary to read the return value of `listener()` although that is slightly more robust. ```php use Crell\Tukio\OrderedListenerProvider; @@ -128,92 +128,257 @@ $provider = new OrderedListenerProvider(); // The ID will be "handleStuff", unless there is such an ID already, //in which case it would be "handleStuff-1" or similar. -$id = $provider->addListener('handleStuff'); +$id = $provider->listener('handleStuff'); // This Listener will get called before handleStuff does. If you want to specify an ID // you can, since anonymous functions just get a random string as their generated ID. -$provider->addListenerBefore($id, function(SpecificStuffHappened) { +$provider->listener($id, function(SpecificStuffHappened) { // ... -}, 'my_specifics'); +}, before: ['my_specifics']); ``` Here, the priority of `handleStuff` is undefined; the user doesn't care when it gets called. However, the anonymous function, if it should get called at all, will always get called after `handleStuff` does. It's possible that some other Listener could also be called in between the two, but one will always happen after the other. -The full API for those three methods is: +The `listener()` method is used for all registration, and can accept a priority, a list of listener IDs the new listener must come before, and a list of listener IDs the new listener must come after. It also supports specifying a custom ID, and a custom `$type`. -```php -public function addListener(callable $listener, $priority = 0, string $id = null, string $type = null): string; - -public function addListenerBefore(string $before, callable $listener, string $id = null, string $type = null): string; +Because that's a not-small number of options, it is *strongly recommended* that you use named arguments for all arguments other than the listener callable itself. -public function addListenerAfter(string $after, callable $listener, string $id = null, string $type = null): string; +```php +public function listener( + callable $listener, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null, + ?string $type = null +): string; ``` -All three can register any callable as a Listener and return an ID. If desired the `$type` parameter allows a user to specify the Event type that the Listener is for if different than the type declaration in the function. For example, if the Listener doesn't have a type declaration or should only apply to some parent class of what it's type declaration is. (That's a rare edge case, which is why it's the last parameter.) +The `listener()` method will always return the ID that was used for that listener. If desired the `$type` parameter allows a user to specify the Event type that the Listener is for if different than the type declaration in the function. For example, if the Listener doesn't have a type declaration or should only apply to some parent class of what it's type declaration is. (That's a rare edge case, which is why it's the last parameter.) #### Service Listeners -Often, though, Listeners are themselves methods of objects that should not be instantiated until and unless needed. That's exactly what a Dependency Injection Container allows, and `OrderedListenerProvider` fully supports those, called "Service Listeners". They work almost exactly the same, except you specify a service and method name: +Often, though, Listeners are themselves methods of objects that should not be instantiated until and unless needed. That's exactly what a Dependency Injection Container allows, and `OrderedListenerProvider` fully supports those, called "Service Listeners." They work almost exactly the same, except you specify a service and method name: ```php -public function addListenerService(string $serviceName, string $methodName, string $type, $priority = 0, string $id = null): string; +public function listenerService( + string $service, + ?string $method = null, + ?string $type = null, + ?int $priority = null, + array $before = [], + array $after = [], + ?string $id = null +): string; +``` -public function addListenerServiceBefore(string $before, string $serviceName, string $methodName, string $type, string $id = null): string; +The `$type`, `$priority`, `$before`, `$after`, and `$id` parameters work the same way as for `listener()`. `$service` is any service name that will be retrieved from a container on-demand, and `$method` is the method on the object. -public function addListenerServiceAfter(string $after, string $serviceName, string $methodName, string $type, string $id = null) : string; -``` +If the service name is the same as that of a found class (which is typical in most modern conventions), then Tukio can attempt to derive the method and type from the class. If the service name is not the same as a defined class, it cannot do so and both `$method` and `$type` are required and will throw an exception if missing. -All three take a service name and method name to identify a Listener. However, since the callable itself doesn't exist yet it's not possible to derive the Event it should listen to through reflection. It must be specified explicitly. All three also return an ID, so the same "priority-or-before/after" functionality is supported. +If no `$method` is specified and the service name matches a class, Tukio will attempt to derive the method for you. If the class has only one method, that method will be automatically selected. Otherwise, if there is a `__invoke()` method, that will be automatically selected. Otherwise, the auto-detection fails and an exception is thrown. The services themselves can be from any [PSR-11](http://www.php-fig.org/psr/psr-11/)-compatible Container. ```php use Crell\Tukio\OrderedListenerProvider; +class SomeService +{ + public function methodA(ThingHappened $event): void { ... } + + public function methodB(SpecificThingHappened $event): void { ... } +} + +class MyListeners +{ + public function methodC(WhatHappened $event): void { ... } + + public function somethingElse(string $beep): string { ... } +} + +class EasyListening +{ + public function __invoke(SpecificThingHappened $event): void { ... } +} + $container = new SomePsr11Container(); // Configure the container somehow. +$container->register('some_service', SomeService::class); +$container->register(MyListeners::class, MyListeners::class); +$container->register(EasyListening::class, EasyListening::class); $provider = new OrderedListenerProvider($container); -$provider->addListenerService('some_service', 'methodA', ThingHappened::class); +// Manually register two methods on the same service. +$idA = $provider->listenerService('some_service', 'methodA', ThingHappened::class); +$idB = $provider->listenerService('some_service', 'methodB', SpecificThingHappened::class); -$id = $provider->addListenerServiceBefore('some_service-methodA', 'some_service', 'methodB', SpecificThingHappened::class); +// Register a specific method on a derivable service class. +// The type (WhatHappened) will be derived automatically. +$idC = $provider->listenerService(MyListeners::class, 'methodC', after: 'some_service-methodB'); -$provider->addListenerServiceAfter($id, 'some_other_service', 'methodC', SpecificThingHappened::class); +// Auto-everything! This is the easiest option. +$provider->listenerService(EasyListening::class, before: $idC); ``` -In this example, we assume that `$container` has two services defined: `some_service` and `some_other_service`. (Creative, I know.) We then register three Listeners: Two of them are methods on `some_service`, the other on `some_other_service`. Both services will be requested from the container as needed, so won't be instantiated until the Listener method is about to be called. +In this example, we have listener methods defined in three different classes, all of which are registered with a PSR-11 container. In the first code block, we register two listeners out of a class whose service name does not match its class name. In the second, we register a method on a class whose service name does match its class name, so we can derive the event type by reflection. In the third block, we use a single-method listener class, which allows everything to be derived! Of note, the `methodB` Listener is referencing the `methodA` listener by an explict ID. The generated ID is as noted predictable, so in most cases you don't need to use the return value. The return value is the more robust and reliable option, though, as if the requested ID is already in-use a new one will be generated. -#### Subscribers +#### Attribute-based registration -As in the last example, it's quite common to have multiple Listeners in a single service. In fact, it's common to have a service that is nothing but listeners. Tukio calls those "Subscribers" (a name openly and unashamedly borrowed from Symfony), and has specific support for them. +The preferred way to configure Tukio, however, is via attributes. There are four relevant attributes: `Listener`, `ListenerPriority`, `ListenerBefore`, and `ListenerAfter`. All can be used with sequential parameters or named parameters. In most cases, named parameters will be more self-documenting. All attributes are valid only on functions and methods. -The basic case works like this: +* `Listener` declares a callable a listener and optionally sets the `id` and `type`: `#[Listener(id: 'a_listener', type: 'SomeClass')]. +* `ListenerPriority` has a required `priority` parameter, and optional `id` and `type: `#[ListenerPriority(5)]` or `#[ListenerPriority(priority: 3, id: "a_listener")]`. +* `ListenerBefore` has a required `before` parameter, and optional `id` and `type: `#[ListenerBefore('other_listener')]` or `#[ListenerBefore(before: 'other_listener', id: "a_listener")]`. +* `ListenerAfter` has a required `after` parameter, and optional `id` and `type: `#[ListenerAfter('other_listener')]` or `#[ListenerAfter(after: ['other_listener'], id: "a_listener")]`. + +The `$before` and `$after` parameters will accept either a single string, or an array of strings. + +As multiple attributes may be included in a single block, that allows for compact syntax like so: ```php -use Crell\Tukio\OrderedListenerProvider; +#[Listener(id: 'a_listener'), ListenerBefore('other'), ListenerAfter('something', 'else')] +function my_listener(SomeEvent $event): void { ... } + +// Or just use the one before/after you care about: +#[ListenerAfter('something_early')] +function other(SomeEvent $event): void { ... } +``` + +If you pass a listener with Listener attributes to `listener()` or `listenerService()`, the attribute defined configuration will be used. If you pass configuration in the method signature, however, that will override any values taken from the attributes. + +### Subscribers -class Subscriber +A "Subscriber" (a name openly and unashamedly borrowed from Symfony) is a class with multiple listener methods on it. Tukio allows you to bulk-register any listener-like methods on a class, just by registering the class. + +```php +$provider->addSubscriber(SomeCollectionOfListeners::class, 'service_name'); +``` + +As before, if the service name is the same as that of the class, it may be omitted. A method will be registered if either: + +* it has any `Listener*` attributes on it. +* the method name begins with `on`. + +For example: + +```php +class SomeCollectionOfListeners { - public function onThingsHappening(ThingHappened $event) : void { ... } + // Registers, with a custom ID. + #[Listener(id: 'a')] + public function onA(CollectingEvent $event) : void + { + $event->add('A'); + } - public function onSpecialEvent(SpecificThingHappened $event) : void { ... } + // Registers, with a custom priority. + #[ListenerPriority(priority: 5)] + public function onB(CollectingEvent $event) : void + { + $event->add('B'); + } + + // Registers, before listener "a" above. + #[ListenerBefore(before: 'a')] + public function onC(CollectingEvent $event) : void + { + $event->add('C'); + } + + // Registers, after listener "a" above. + #[ListenerAfter(after: 'a')] + public function onD(CollectingEvent $event) : void + { + $event->add('D'); + } + + // This still self-registers because of the name. + public function onE(CollectingEvent $event) : void + { + $event->add('E'); + } + + // Registers, with a given priority despite its non-standard name. + #[ListenerPriority(priority: -5)] + public function notNormalName(CollectingEvent $event) : void + { + $event->add('F'); + } + + // No attribute, non-standard name, this method is not registered. + public function ignoredMethodThatDoesNothing() : void + { + throw new \Exception('What are you doing here?'); + } } +``` -$container = new SomePsr11Container(); -// Configure the container so that the service 'listeners' is an instance of Subscriber above. +### Listener classes -$provider = new OrderedListenerProvider($container); +As hinted above, one of the easiest ways to structure a listener is to make it the only method on a class, particularly if it is named `__invoke()`, and give the service the same name as the class. That way, it can be registered trivially and derive all of its configuration through attributes. Since it is extremely rare for a listener to be registered twice (use cases likely exist, but we are not aware of one), this does not cause a name collision issue. +Tukio has two additional features to make it even easier. One, if the listener method is `__invoke()`, then the ID of the listener will by default be just the class name. Two, the Listener attributes may also be placed on the class, not the method, in which case the class-level settings will inherit to every method. -$provider->addSubscriber(Subscriber::class, 'listeners'); +The result is that the easiest way to define listeners is as single-method classes, like so: + +```php +class ListenerOne +{ + public function __construct( + private readonly DependencyA $depA, + private readonly DependencyB $depB, + ) {} + + public function __invoke(MyEvent $event): void { ... } +} + +#[ListenerBefore(ListenerOne::class)] +class ListenerTwo +{ + public function __invoke(MyEvent $event): void { ... } +} + +$provider->listenerService(ListenerOne::class); +$provider->listenerService(ListenerTwo::class); ``` -That's it! Because we don't know what the class of the service is it will need to be specified explicitly, but the rest is automatic. Any public method whose name begins with "on" will be registered as a listener, and the Event type it is for will be derived from reflection, while the rest of the class is ignored. There is no limit to how many listeners can be added this way. The method names can be anything that makes sense in context, so make it descriptive. And because the service is pulled on-demand from the container it will only be instantiated once, and not before it's needed. That's the ideal. +Now, the API call itself is trivially easy. Just specify the class name. `ListenerTwo::__invoke()` will be called before `ListnerOne::__invoke()`, regardless of the order in which they were registered. When `ListenerOne` is requested from your DI container, the container will fill in its dependencies automatically. + +This is the recommended way to write listeners for use with Tukio. + +### Deprecated functionality + +A few registration mechanisms left over from Tukio version 1 are still present, but explicitly deprecated. They will be removed in a future version. Please migrate off of them as soon as possible. -Sometimes, though, you want to order the Listeners in the Subscriber, or need to use a different naming convention for the Listener method. For that case, your Subscriber class can implement an extra interface that allows for manual registration: +#### Dedicated registration methods + +The following methods still work, but are just aliases around calling `listener()` or `listenerService()`. They are less capable than just using `listener()`, as `listener()` allows for specifying a priority, before, and after all at once, including multiple before/after targets. The methods below do neither. Please migrate to `listener()` and `listenerService()`. + +```php +public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string; + +public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string; + +public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string; + +public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string; + +public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string; + +public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string; +``` + +#### Subscriber interface + +In Tukio v1, there was an optional `SubscriberInterface` to allow for customizing the registration of methods as listeners via a static method that bundled the various `addListener*()` calls up within the class. With the addition of attributes in PHP 8, however, that functionality is no longer necessary as attributes can do everything the Subscriber interface could, with less work. + +The `SubscriberInterface` is still supported, but deprecated. It will be removed in a future version. Please migrate to attributes. + +The basic case works like this: ```php use Crell\Tukio\OrderedListenerProvider; @@ -244,17 +409,13 @@ $provider->addSubscriber(Subscriber::class, 'listeners'); As before, `onThingsHappen()` will be registered automatically. However, `somethingElse()` will also be registered as a Listener with a priority of 10, and `onSpecialEvent()` will be registered to fire after it. -In practice, using Subscribers is the most robust way to register Listeners in a production system and so is the recommended approach. However, all approaches have their uses and can be used as desired. - -If you are using PHP 8.0 or later, you can use attributes to register your subscriber methods instead. That is preferred, in fact. See the section below. - ### Compiled Provider All of that registration and ordering logic is powerful, and it's surprisingly fast in practice. What's even faster, though, is not having to re-register on every request. For that, Tukio offers a compiled provider option. The compiled provider comes in three parts: `ProviderBuilder`, `ProviderCompiler`, and a generated provider class. `ProviderBuilder`, as the name implies, is an object that allows you to build up a set of Listeners that will make up a Provider. They work exactly the same as on `OrderedListenerProvider`, and in fact it exposes the same `OrderedProviderInterface`. -`ProviderCompiler` then takes a builder object and writes a new PHP class to a provided stream (presumably a file on disk) that matches the definitions in the builder. That built Provider is fixed; it cannot be modified and no new Listeners can be added to it, but all of the ordering and sorting has already been done, making it notably faster (to say nothing of skipping the registration process itself). +`ProviderCompiler` then takes a builder object and writes a new PHP class to a provided stream (presumably a file on disk) that matches the definitions in the builder. That built Provider is fixed; it cannot be modified and no new Listeners can be added to it, but all the ordering and sorting has already been done, making it notably faster (to say nothing of skipping the registration process itself). Let's see it in action: @@ -264,11 +425,11 @@ use Crell\Tukio\ProviderCompiler; $builder = new ProviderBuilder(); -$builder->addListener('listenerA', 100); -$builder->addListenerAfter('listenerA', 'listenerB'); -$builder->addListener([Listen::class, 'listen']); -$builder->addListenerService('listeners', 'listen', CollectingEvent::class); -$builder->addSubscriber('subscriber', Subscriber::class); +$builder->listener('listenerA', priority: 100); +$builder->listener('listenerB', after: 'listenerA'); +$builder->listener([Listen::class, 'listen']); +$builder->listenerService(MyListener::class); +$builder->addSubscriber('subscriberId', Subscriber::class); $compiler = new ProviderCompiler(); @@ -300,6 +461,26 @@ $provider = new Name\Space\Of\My\App\MyCompiledProvider($container); And boom! `$provider` is now a fully functional Provider you can pass to a Dispatcher. It will work just like any other, but faster. +Alternatively, the compiler can output a file with an anonymous class. In this case, a class name or namespace are irrelevant. + +```php +// Write the generated compiler out to a file. +$filename = 'MyCompiledProvider.php'; +$out = fopen($filename, 'w'); + +$compiler->compileAnonymous($builder, $out); + +fclose($out); +``` + +Because the compiled container will be instantiated by including a file, but it needs a container instance to function, it cannot be easily just `require()`ed. Instead, use the `loadAnonymous()` method on a `ProviderCompiler` instance to load it. (It does not need to be the same instance that was used to create it.) + +```php +$compiler = new ProviderCompiler(); + +$provider = $compiler->loadAnonymous($filename, $containerInstance); +``` + But what if you want to have most of your listeners pre-registered, but have some that you add conditionally at runtime? Have a look at the FIG's [`AggregateProvider`](https://github.com/php-fig/event-dispatcher-util/blob/master/src/AggregateProvider.php), and combine your compiled Provider with an instance of `OrderedListenerProvider`. ### Compiler optimization @@ -312,11 +493,11 @@ use Crell\Tukio\ProviderCompiler; $builder = new ProviderBuilder(); -$builder->addListener('listenerA', 100); -$builder->addListenerAfter('listenerA', 'listenerB'); -$builder->addListener([Listen::class, 'listen']); -$builder->addListenerService('listeners', 'listen', CollectingEvent::class); -$builder->addSubscriber('subscriber', Subscriber::class); +$builder->listener('listenerA', priority: 100); +$builder->listener('listenerB', after: 'listenerA'); +$builder->listener([Listen::class, 'listen']); +$builder->listenerService(MyListener::class); +$builder->addSubscriber('subscriberId', Subscriber::class); // Here's where you specify what events you know you will have. // Returning the listeners for these events will be near instant. @@ -329,82 +510,11 @@ $compiler = new ProviderCompiler(); $filename = 'MyCompiledProvider.php'; $out = fopen($filename, 'w'); -// Here's the magic: -$compiler->compile($builder, $out, 'MyCompiledProvider', '\\Name\\Space\\Of\\My\\App'); +$compiler->compileAnonymous($builder, $out); fclose($out); ``` -### Attribute-based registration - -If you are using PHP 8.0 or later, Tukio fully supports attributes as a means of registration for both `OrderedListenerProvider` and `ProviderBuilder`. There are four relevant attributes: `Listener`, `ListenerPriority`, `ListenerBefore`, and `ListenerAfter`. All can be used with sequential parameters or named parameters. In most cases, named parameters will be more self-documenting. All attributes are valid only on functions and methods. - -* `Listener` declares a callable a listener and optionally sets the `id` and `type`: `#[Listener(id: 'a_listener', type: `SomeClass`)]. -* `ListenerPriority` has a required `priority` parameter, and optional `id` and `type: `#[ListenerPriority(5)]` or `#[ListeenPriority(priority: 3, id: "a_listener")]`. -* `ListenerBefore` has a required `before` parameter, and optional `id` and `type: `#[ListenerBefore('other_listener')]` or `#[ListenerBefore(before: 'other_listener', id: "a_listener")]`. -* `ListenerAfter` has a required `after` parameter, and optional `id` and `type: `#[ListenerAfter('other_listener')]` or `#[ListenerAfter(after: 'other_listener', id: "a_listener")]`. - -Each attribute matches a corresponding method, and the values in the attribute will be used as if they were passed directly to the `addListener*()` method. If a value is passed directly to `addListener()` and specified in the attribute, the directly-passed value takes precedence. - -If you call `addListenerBefore()`/`addListenerAfter()`, the `before`/`after`/`priority` attribute parameter is ignored in favor of the method's standard behavior. That is, `addListenerBefore()`, if called with a function that has a `#[ListenerAfter]` attribute, will still add the listener "before" the specified other listener. - -There are two common use cases for attributes: One, using just `#[Listener]` to define a custom `id` or `type` and then calling the appropriate `add*` method as normal. The other is on Subscribers, where attributes completely eliminate the need for the `registerListeners()` method. If you're on PHP 8.0, please use attributes instead of `registerListeners()`. It's not deprecated yet, but it may end up deprecated sometime in the future. - -For example: - -```php -class SomeSubscriber -{ - // Registers, with a custom ID. - #[Listener(id: 'a')] - public function onA(CollectingEvent $event) : void - { - $event->add('A'); - } - - // Registers, with a custom priority. - #[ListenerPriority(priority: 5)] - public function onB(CollectingEvent $event) : void - { - $event->add('B'); - } - - // Registers, before listener "a" above. - #[ListenerBefore(before: 'a')] - public function onC(CollectingEvent $event) : void - { - $event->add('C'); - } - - // Registers, after listener "a" above. - #[ListenerAfter(after: 'a')] - public function onD(CollectingEvent $event) : void - { - $event->add('D'); - } - - // This still self-registers because of the name. - public function onE(CollectingEvent $event) : void - { - $event->add('E'); - } - - // Registers, with a given priority despite its non-standard name. - #[ListenerPriority(priority: -5)] - public function notNormalName(CollectingEvent $event) : void - { - $event->add('F'); - } - - // No attribute, non-standard name, this method is not registered. - public function ignoredMethodThatDoesNothing() : void - { - throw new \Exception('What are you doing here?'); - } -} -``` - - ### `CallbackProvider` The third option Tukio provides is a `CallbackProvider`, which takes an entirely different approach. In this case, the Provider works only on events that have a `CallbackEventInterface`. The use case is for Events that are carrying some other object, which itself has methods on it that should be called at certain times. Think lifecycle callbacks for a domain object, for example. @@ -477,7 +587,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT ## Security -If you discover any security related issues, please email larry@garfieldtech.com instead of using the issue tracker. +If you discover any security related issues, please use the [GitHub security reporting form](https://github.com/Crell/Tukio/security) rather than the issue queue. ## Credits From 41a34df8e5d79ecc5cec58f6d9eaec417696ef67 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:25:45 -0500 Subject: [PATCH 74/77] Update CHANGELOG. --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24772cf..16a408e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,14 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Major internal refactoring. - There is now a `listener()` method on the Provider and Compiler classes that allows specifying multiple before/after rules at once, in addition to priority. It is *recommended* to use this method in place of the older ones. +- Similarly, there is a `listenerService()` method for registering any service-based listener. - Upgraded to OrderedCollection v2, and switched to a Topological-based sort. The main advantage is the ability to support multiple before/after rules. However, this has a side effect that the order of listeners that had no relative order specified may have changed. This is not an API break as that order was never guaranteed, but may still affect some order-sensitive code that worked by accident. If that happens, and you care about the order, specify before/after orders as appropriate. +- Attributes are now the recommended way to register listeners. +- Attributes may be placed on the class level, and will be inherited by method-level listeners. ### Deprecated -- Nothing +- `SubscriberInterface` is now deprecated. It will be removed in v3. +- the `addListener`, `addListenerBefore`, `addListenerAfter`, `addListenerService`, `addListenerServiceBefore`, and `addListenerServiceAfter` methods have been deprecated. They will be removed in v3. Use `listener()` and `listenerService()` instead. ### Fixed - Nothing From 09b541786c598264a5990d04760e42bbcf73b015 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:25:52 -0500 Subject: [PATCH 75/77] Small fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25e3a88..260d962 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ $provider->listener(function(SpecificStuffHappened) { // ... }, priority: 10); -$provider->addListener('handleStuff', priority: 20); +$provider->listener('handleStuff', priority: 20); ``` Now, the named function Listener will get called before the anonymous function does. (Higher priority number comes first, and negative numbers are totally legal.) If two listeners have the same priority then their order relative to each other is undefined. From 8bba3125db15c18937df67361615ed7b8d1a792e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 22 Mar 2024 19:27:27 -0500 Subject: [PATCH 76/77] Minor test improvement. --- tests/CompiledListenerProviderTest.php | 2 +- tests/Listeners/InvokableListenerClassNoId.php | 2 ++ tests/OrderedListenerProviderServiceTest.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/CompiledListenerProviderTest.php b/tests/CompiledListenerProviderTest.php index 4f0c8fe..592f870 100644 --- a/tests/CompiledListenerProviderTest.php +++ b/tests/CompiledListenerProviderTest.php @@ -333,7 +333,7 @@ public function detects_invoke_method_and_gives_class_id_by_default(): void $builder = new ProviderBuilder(); $container = new MockContainer(); - $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId()); + $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId('beep')); $container->addService(InvokableListenerClassNoIdBefore::class, new InvokableListenerClassNoIdBefore()); $builder->listenerService(InvokableListenerClassNoId::class); diff --git a/tests/Listeners/InvokableListenerClassNoId.php b/tests/Listeners/InvokableListenerClassNoId.php index b10b855..79879c1 100644 --- a/tests/Listeners/InvokableListenerClassNoId.php +++ b/tests/Listeners/InvokableListenerClassNoId.php @@ -8,6 +8,8 @@ #[ListenerPriority(priority: 5)] class InvokableListenerClassNoId { + public function __construct(public readonly string $val) {} + public function __invoke(CollectingEvent $event): void { $event->add(static::class); diff --git a/tests/OrderedListenerProviderServiceTest.php b/tests/OrderedListenerProviderServiceTest.php index cdb5a85..19eddc4 100644 --- a/tests/OrderedListenerProviderServiceTest.php +++ b/tests/OrderedListenerProviderServiceTest.php @@ -295,7 +295,7 @@ public function detects_invoke_method_and_type_with_class_attribute(): void public function detects_invoke_method_and_gives_class_id_by_default(): void { $container = new MockContainer(); - $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId()); + $container->addService(InvokableListenerClassNoId::class, new InvokableListenerClassNoId('beep')); $provider = new OrderedListenerProvider($container); From 954ac170d608e4c4e2fe67aaca37b69d41db726c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 7 Apr 2024 21:08:58 -0400 Subject: [PATCH 77/77] Use new stable version of OrderedCollection. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 76aedb5..3717dac 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require": { "php": "~8.1", "crell/attributeutils": "^1.1", - "crell/ordered-collection": "v2.x-dev", + "crell/ordered-collection": "~2.0", "fig/event-dispatcher-util": "^1.3", "psr/container": "^1.0 || ^2.0", "psr/event-dispatcher": "^1.0",