Skip to content

Commit

Permalink
Allow for listening for parent classes or interfaces (#196)
Browse files Browse the repository at this point in the history
* Refactor to a single `$targets` array

* Fix styling
  • Loading branch information
inxilpro authored Jan 6, 2025
1 parent 5c2ed3c commit 28c9bc1
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/Attributes/Hooks/Apply.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public function __construct(

public function applyToHook(Hook $hook): void
{
$hook->states[] = $this->state_type;
$hook->targets[] = $this->state_type;
}
}
2 changes: 1 addition & 1 deletion src/Attributes/Hooks/Listen.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public function __construct(

public function applyToHook(Hook $hook): void
{
$hook->events[] = $this->event_type;
$hook->targets[] = $this->event_type;
}
}
11 changes: 5 additions & 6 deletions src/Lifecycle/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -185,7 +182,9 @@ protected function hooksWithPrefix(Event|State $target, Phase $phase, string $pr
/** @return Collection<int, Hook> */
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)));
}

Expand Down
9 changes: 3 additions & 6 deletions src/Lifecycle/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);

Expand All @@ -34,17 +33,15 @@ 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);
}

public function __construct(
public Closure $callback,
public array $events = [],
public array $states = [],
public array $targets = [],
public SplObjectStorage $phases = new SplObjectStorage,
public ?string $name = null,
) {}
Expand Down
2 changes: 1 addition & 1 deletion src/Lifecycle/StateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions src/Support/Reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
60 changes: 60 additions & 0 deletions tests/Feature/HooksClassHierarchyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

use Thunk\Verbs\Attributes\Hooks\On;
use Thunk\Verbs\Event;
use Thunk\Verbs\Lifecycle\Dispatcher;
use Thunk\Verbs\Lifecycle\Phase;

it('can match events by type-hinting a specific class', function () {
app(Dispatcher::class)->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');
}
}
7 changes: 4 additions & 3 deletions tests/Unit/UseStatesDirectlyInEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}
Expand Down

0 comments on commit 28c9bc1

Please sign in to comment.