From 60e6b54648301b1f8aa91020599737f28544e2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 1 Apr 2024 11:54:25 +0200 Subject: [PATCH] Allow hooks to be updated when active (#246) --- src/HookTrait.php | 44 ++++++++++--- tests/HookTraitTest.php | 136 ++++++++++++++++++++++++++++++---------- 2 files changed, 138 insertions(+), 42 deletions(-) diff --git a/src/HookTrait.php b/src/HookTrait.php index d78b031a..52fd4d01 100644 --- a/src/HookTrait.php +++ b/src/HookTrait.php @@ -6,11 +6,7 @@ trait HookTrait { - /** - * Contains information about configured hooks (callbacks). - * - * @var array}>>> - */ + /** @var array}>>> Configured hooks (callbacks). */ protected array $hooks = []; /** Next hook index counter. */ @@ -308,18 +304,48 @@ public function hook(string $spot, array $args = [], &$brokenBy = null) if (isset($this->hooks[$spot])) { krsort($this->hooks[$spot]); // lower priority is called sooner $hooksBackup = $this->hooks[$spot]; + $priorities = array_keys($hooksBackup); + try { - while ($hooks = array_pop($this->hooks[$spot])) { - foreach ($hooks as $index => [$hookFx, $hookArgs]) { + while (($priority = array_pop($priorities)) !== null) { + $hooks2Backup = $this->hooks[$spot][$priority]; + $indexes = array_reverse(array_keys($hooks2Backup)); + + while (($index = array_pop($indexes)) !== null) { + [$hookFx, $hookArgs] = $this->hooks[$spot][$priority][$index]; + $return[$index] = $hookFx($this, ...$args, ...$hookArgs); + + if (!isset($this->hooks[$spot][$priority])) { + break; + } elseif ($hooks2Backup !== $this->hooks[$spot][$priority]) { + $hooks2Backup = $this->hooks[$spot][$priority]; + $indexes = array_reverse(array_keys($hooks2Backup)); + foreach ($indexes as $k => $i) { + if ($i <= $index) { + unset($indexes[$k]); + } + } + } + } + + if (!isset($this->hooks[$spot])) { // @phpstan-ignore-line + break; + } elseif ($hooksBackup !== $this->hooks[$spot]) { + krsort($this->hooks[$spot]); + $hooksBackup = $this->hooks[$spot]; + $priorities = array_keys($hooksBackup); + foreach ($priorities as $k => $p) { + if ($p <= $priority) { + unset($priorities[$k]); + } + } } } } catch (HookBreaker $e) { $brokenBy = $e; return $e->getReturnValue(); - } finally { - $this->hooks[$spot] = $hooksBackup; } } diff --git a/tests/HookTraitTest.php b/tests/HookTraitTest.php index d256f63e..0bf2651c 100644 --- a/tests/HookTraitTest.php +++ b/tests/HookTraitTest.php @@ -63,7 +63,8 @@ public function testHookException1(): void public function testOrder(): void { $m = new HookMock(); - $ind = $m->onHook('spot', static function () { + + $m->onHook('spot', static function () { return 3; }, [], -1); $m->onHook('spot', static function () { @@ -100,16 +101,16 @@ public function testOrder(): void $ret = $m->hook('spot'); self::assertSame([ - $ind + 2 => 1, - $ind + 1 => 2, - $ind => 3, - $ind + 3 => 4, - $ind + 4 => 5, - $ind + 6 => 6, - $ind + 7 => 7, - $ind + 8 => 8, - $ind + 9 => 9, - $ind + 5 => 10, + 2 => 1, + 1 => 2, + 0 => 3, + 3 => 4, + 4 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => 9, + 5 => 10, ], $ret); } @@ -134,6 +135,72 @@ public function testMulti(): void self::assertSame([9, 6], $res2); } + public function testUpdateWhenActive(): void + { + $m = new HookMock(); + + $addHooksFx = static function (int $priority, string $res) use ($m) { + $m->onHook('spot', static function () use ($m, $priority, $res) { + $m->onHook('spot', static function () use ($res) { + return $res . 'a'; + }, [], $priority); + $m->onHook('spot', static function () use ($m, $priority, $res) { + $m->removeHook('spot', $priority); + + return $res . 'b'; + }, [], $priority); + + return $res; + }, [], $priority); + }; + + $addHooksFx(-2, '1'); + $addHooksFx(-1, '2'); + $addHooksFx(-1, '3'); + $addHooksFx(0, '4'); + $addHooksFx(1, '5'); + $addHooksFx(1, '6'); + + $priority = 10; + $indexPrevious = $m->onHook('spot', static fn () => 'x', [], $priority); + $indexCurrent = $m->onHook('spot', static function () use ($m, $priority, $indexPrevious, &$indexCurrent, &$indexNext) { + $m->onHook('spot', static fn () => 'ya', [], $priority); + + $m->removeHook('spot', $indexPrevious, true); + $m->removeHook('spot', $indexCurrent, true); + $m->removeHook('spot', $indexNext, true); // @phpstan-ignore-line + + $m->onHook('spot', static function () use ($m, $priority) { + $m->removeHook('spot', $priority); + + return 'yb'; + }, [], $priority); + + return 'y'; + }, [], $priority); + $indexNext = $m->onHook('spot', static fn () => 'z', [], $priority); + + $ret = $m->hook('spot'); + + self::assertSame([ + 0 => '1', + 10 => '1b', + 2 => '3', + 12 => '3b', + 3 => '4', + 13 => '4a', + 14 => '4b', + 4 => '5', + 5 => '6', + 15 => '5a', + 16 => '5b', + 6 => 'x', + 7 => 'y', + 19 => 'ya', + 20 => 'yb', + ], $ret); + } + public function testArgs(): void { $obj = new HookMock(); @@ -256,9 +323,8 @@ public function testOnHookShort(): void $m->hook('inc', ['y']); } - public function testCloningSafety(): void + public function testCloningSafetyUnboundClosure(): void { - // unbound callback $m = new HookMock(); $m->onHook('inc', static function () {}); $m->onHookShort('inc', static function () {}); @@ -267,8 +333,10 @@ public function testCloningSafety(): void foreach ($m->hook('inc') as $v) { self::assertNull($v); } + } - // callback bound to the same object + public function testCloningSafetyBoundClosureSameObject(): void + { $m = new HookMock(); $m->onHook('inc', $m->makeIncrementResultFx()); $m->onHookShort('inc', $m->makeIncrementResultFx()); @@ -286,8 +354,10 @@ public function testCloningSafety(): void self::assertSame($m, $v); } self::assertSame(6, $m->result); + } - // callback bound to a different object + public function testCloningSafetyBoundClosureDifferentObjectException(): void + { $m = new HookMock(); $m->onHook('inc', (clone $m)->makeIncrementResultFx()); $m = clone $m; @@ -438,7 +508,7 @@ public function testPassByReference(): void public function testHasCallbacks(): void { $m = new HookMock(); - $ind = $m->onHook('foo', static function () {}); + $index = $m->onHook('foo', static function () {}); self::assertTrue($m->hookHasCallbacks('foo')); self::assertFalse($m->hookHasCallbacks('bar')); @@ -447,32 +517,32 @@ public function testHasCallbacks(): void self::assertFalse($m->hookHasCallbacks('foo', 10)); self::assertFalse($m->hookHasCallbacks('bar', 5)); - self::assertTrue($m->hookHasCallbacks('foo', $ind, true)); - self::assertFalse($m->hookHasCallbacks('foo', $ind + 1, true)); - self::assertFalse($m->hookHasCallbacks('foo', $ind - 1, true)); - self::assertFalse($m->hookHasCallbacks('bar', $ind, true)); + self::assertTrue($m->hookHasCallbacks('foo', $index, true)); + self::assertFalse($m->hookHasCallbacks('foo', $index + 1, true)); + self::assertFalse($m->hookHasCallbacks('foo', $index - 1, true)); + self::assertFalse($m->hookHasCallbacks('bar', $index, true)); } public function testRemove(): void { $m = new HookMock(); - $indA = $m->onHook('foo', static function () {}, [], 2); - $indB = $m->onHook('foo', static function () {}); - $indC = $m->onHook('foo', static function () {}); + $indexA = $m->onHook('foo', static function () {}, [], 2); + $indexB = $m->onHook('foo', static function () {}); + $indexC = $m->onHook('foo', static function () {}); - self::assertTrue($m->hookHasCallbacks('foo', $indA, true)); - self::assertTrue($m->hookHasCallbacks('foo', $indB, true)); - self::assertTrue($m->hookHasCallbacks('foo', $indC, true)); + self::assertTrue($m->hookHasCallbacks('foo', $indexA, true)); + self::assertTrue($m->hookHasCallbacks('foo', $indexB, true)); + self::assertTrue($m->hookHasCallbacks('foo', $indexC, true)); - $m->removeHook('foo', $indC, true); - self::assertTrue($m->hookHasCallbacks('foo', $indA, true)); - self::assertTrue($m->hookHasCallbacks('foo', $indB, true)); - self::assertFalse($m->hookHasCallbacks('foo', $indC, true)); + $m->removeHook('foo', $indexC, true); + self::assertTrue($m->hookHasCallbacks('foo', $indexA, true)); + self::assertTrue($m->hookHasCallbacks('foo', $indexB, true)); + self::assertFalse($m->hookHasCallbacks('foo', $indexC, true)); $m->removeHook('foo', 2); - self::assertFalse($m->hookHasCallbacks('foo', $indA, true)); - self::assertTrue($m->hookHasCallbacks('foo', $indB, true)); - self::assertFalse($m->hookHasCallbacks('foo', $indC, true)); + self::assertFalse($m->hookHasCallbacks('foo', $indexA, true)); + self::assertTrue($m->hookHasCallbacks('foo', $indexB, true)); + self::assertFalse($m->hookHasCallbacks('foo', $indexC, true)); self::assertTrue($m->hookHasCallbacks('foo')); $m->removeHook('foo');