diff --git a/src/Attributes/Hooks/Apply.php b/src/Attributes/Hooks/Apply.php index 0b5e6b31..6e3b9fa7 100644 --- a/src/Attributes/Hooks/Apply.php +++ b/src/Attributes/Hooks/Apply.php @@ -20,6 +20,6 @@ public function __construct( public function applyToHook(Hook $hook): void { - $hook->states[] = $this->state_type; + $hook->targets[] = $this->state_type; } } diff --git a/src/Attributes/Hooks/Listen.php b/src/Attributes/Hooks/Listen.php index 270a751a..ce6fb30d 100644 --- a/src/Attributes/Hooks/Listen.php +++ b/src/Attributes/Hooks/Listen.php @@ -20,6 +20,6 @@ public function __construct( public function applyToHook(Hook $hook): void { - $hook->events[] = $this->event_type; + $hook->targets[] = $this->event_type; } } diff --git a/src/Lifecycle/Dispatcher.php b/src/Lifecycle/Dispatcher.php index 8a36cb71..e54736a8 100644 --- a/src/Lifecycle/Dispatcher.php +++ b/src/Lifecycle/Dispatcher.php @@ -24,11 +24,8 @@ public function __construct( public function register(object $target): void { foreach (Reflector::getHooks($target) as $hook) { - foreach ($hook->events as $event_type) { - $this->hooks[$event_type][] = $hook; - } - foreach ($hook->states as $state_type) { - $this->hooks[$state_type][] = $hook; + foreach ($hook->targets as $fqcn) { + $this->hooks[$fqcn][] = $hook; } } } @@ -185,7 +182,9 @@ protected function hooksWithPrefix(Event|State $target, Phase $phase, string $pr /** @return Collection */ protected function hooksFor(Event|State $target, ?Phase $phase = null): Collection { - return Collection::make($this->hooks[$target::class] ?? []) + return Collection::make($this->hooks) + ->only(Reflector::getClassInstanceOf($target)) + ->flatten() ->when($phase, fn (Collection $hooks) => $hooks->filter(fn (Hook $hook) => $hook->runsInPhase($phase))); } diff --git a/src/Lifecycle/Hook.php b/src/Lifecycle/Hook.php index 9d757588..5ebbdb85 100644 --- a/src/Lifecycle/Hook.php +++ b/src/Lifecycle/Hook.php @@ -22,8 +22,7 @@ public static function fromClassMethod(object $target, ReflectionMethod|string $ $hook = new static( callback: Closure::fromCallable([$target, $method->getName()]), - events: Reflector::getEventParameters($method), - states: Reflector::getStateParameters($method), + targets: Reflector::getParameterTypes($method), name: $method->getName(), ); @@ -34,8 +33,7 @@ public static function fromClosure(Closure $callback): static { $hook = new static( callback: $callback, - events: Reflector::getEventParameters($callback), - states: Reflector::getStateParameters($callback), + targets: Reflector::getParameterTypes($callback), ); return Reflector::applyHookAttributes($callback, $hook); @@ -43,8 +41,7 @@ public static function fromClosure(Closure $callback): static public function __construct( public Closure $callback, - public array $events = [], - public array $states = [], + public array $targets = [], public SplObjectStorage $phases = new SplObjectStorage, public ?string $name = null, ) {} diff --git a/src/Lifecycle/StateManager.php b/src/Lifecycle/StateManager.php index 344448ab..26aef786 100644 --- a/src/Lifecycle/StateManager.php +++ b/src/Lifecycle/StateManager.php @@ -194,7 +194,7 @@ protected function reconstitute(State $state, bool $singleton = false): static // them from snapshots as needed in the rest of the request. // FIXME: We still need to figure this out // $this->states->reset(); - //$this->remember($state); + // $this->remember($state); } return $this; diff --git a/src/Support/Reflector.php b/src/Support/Reflector.php index 8f65e097..7560062e 100644 --- a/src/Support/Reflector.php +++ b/src/Support/Reflector.php @@ -42,6 +42,23 @@ public static function getStateParameters(ReflectionFunctionAbstract|Closure $me return static::getParametersOfType(State::class, $method)->values()->all(); } + public static function getParameterTypes(ReflectionFunctionAbstract|Closure $method): array + { + $method = static::reflectFunction($method); + + if (empty($parameters = $method->getParameters())) { + return []; + } + + return Collection::make($parameters) + ->map(fn (ReflectionParameter $parameter) => static::getParameterClassNames($parameter)) + ->flatten() + ->filter() + ->unique() + ->values() + ->toArray(); + } + public static function applyHookAttributes(ReflectionFunctionAbstract|Closure $method, Hook $hook): Hook { $method = static::reflectFunction($method); @@ -80,6 +97,20 @@ public static function getParametersOfType(string $type, ReflectionFunctionAbstr ->map(fn (array $names) => Arr::first($names)); } + /** @return class-string[] */ + public static function getClassInstanceOf(string|object $class): array + { + $reflection = new ReflectionClass($class); + + $class_and_interface_names = array_unique($reflection->getInterfaceNames()); + + do { + $class_and_interface_names[] = $reflection->getName(); + } while ($reflection = $reflection->getParentClass()); + + return $class_and_interface_names; + } + protected static function reflectFunction(ReflectionFunctionAbstract|Closure $function): ReflectionFunctionAbstract { if ($function instanceof Closure) { diff --git a/tests/Feature/HooksClassHierarchyTest.php b/tests/Feature/HooksClassHierarchyTest.php new file mode 100644 index 00000000..e52f83eb --- /dev/null +++ b/tests/Feature/HooksClassHierarchyTest.php @@ -0,0 +1,60 @@ +register(new HooksClassHierarchyTestOneListener); + + expect(fn () => HooksClassHierarchyTestEvent1::fire())->toThrow(RuntimeException::class, 'one') + ->and(fn () => HooksClassHierarchyTestEvent2::fire())->not->toThrow(RuntimeException::class); +}); + +it('can match events by type-hinting an interface', function () { + app(Dispatcher::class)->register(new HooksClassHierarchyTestInterfaceListener); + + expect(fn () => HooksClassHierarchyTestEvent1::fire())->not->toThrow(RuntimeException::class) + ->and(fn () => HooksClassHierarchyTestEvent2::fire())->toThrow(RuntimeException::class, 'interface'); +}); + +it('can match all events by type-hinting the base class', function () { + app(Dispatcher::class)->register(new HooksClassHierarchyTestEveryListener); + + expect(fn () => HooksClassHierarchyTestEvent1::fire())->toThrow(RuntimeException::class, 'every') + ->and(fn () => HooksClassHierarchyTestEvent2::fire())->toThrow(RuntimeException::class, 'every'); +}); + +interface HooksClassHierarchyTestInterface {} + +class HooksClassHierarchyTestEvent1 extends Event {} + +class HooksClassHierarchyTestEvent2 extends Event implements HooksClassHierarchyTestInterface {} + +class HooksClassHierarchyTestOneListener +{ + #[On(Phase::Validate)] + public static function one(HooksClassHierarchyTestEvent1 $event) + { + throw new RuntimeException('one'); + } +} + +class HooksClassHierarchyTestInterfaceListener +{ + #[On(Phase::Validate)] + public static function interface(HooksClassHierarchyTestInterface $event) + { + throw new RuntimeException('interface'); + } +} + +class HooksClassHierarchyTestEveryListener +{ + #[On(Phase::Validate)] + public static function every(Event $event) + { + throw new RuntimeException('every'); + } +} diff --git a/tests/Unit/UseStatesDirectlyInEventsTest.php b/tests/Unit/UseStatesDirectlyInEventsTest.php index b1c6a5ed..dc127e43 100644 --- a/tests/Unit/UseStatesDirectlyInEventsTest.php +++ b/tests/Unit/UseStatesDirectlyInEventsTest.php @@ -101,7 +101,7 @@ $this->assertEquals($event2->id, $user_request2->last_event_id); }); -it('supports union typed properties in events', function() { +it('supports union typed properties in events', function () { $user_request = UserRequestState::new(); UserRequestsWithUnionTypes::commit( @@ -169,14 +169,15 @@ public function apply() } } -class UserRequestsWithUnionTypes extends Event +class UserRequestsWithUnionTypes extends Event { public function __construct( public UserRequestState $user_request, public string|int $value ) {} - public function apply() { + public function apply() + { $this->user_request->unionTypedValue = $this->value; } }