From c45dadcec8b665834ddf9ca60bcffc0b763f4001 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:57:36 +0000 Subject: [PATCH 01/10] Add coroutine-safe Once class Implements Once using Hyperf Context for per-coroutine isolation, ensuring concurrent requests don't interfere with cached values. --- src/support/src/Once.php | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/support/src/Once.php diff --git a/src/support/src/Once.php b/src/support/src/Once.php new file mode 100644 index 000000000..a1d95c944 --- /dev/null +++ b/src/support/src/Once.php @@ -0,0 +1,87 @@ +> $values + */ + protected function __construct(protected WeakMap $values) + { + // + } + + /** + * Create a new once instance. + */ + public static function instance(): static + { + return Context::getOrSet(self::INSTANCE_CONTEXT_KEY, fn () => new static(new WeakMap())); + } + + /** + * Get the value of the given onceable. + */ + public function value(Onceable $onceable): mixed + { + if (Context::get(self::ENABLED_CONTEXT_KEY, true) !== true) { + return call_user_func($onceable->callable); + } + + $object = $onceable->object ?: $this; + + $hash = $onceable->hash; + + if (! isset($this->values[$object])) { + $this->values[$object] = []; + } + + if (array_key_exists($hash, $this->values[$object])) { + return $this->values[$object][$hash]; + } + + return $this->values[$object][$hash] = call_user_func($onceable->callable); + } + + /** + * Re-enable the once instance if it was disabled. + */ + public static function enable(): void + { + Context::set(self::ENABLED_CONTEXT_KEY, true); + } + + /** + * Disable the once instance. + */ + public static function disable(): void + { + Context::set(self::ENABLED_CONTEXT_KEY, false); + } + + /** + * Flush the once instance. + */ + public static function flush(): void + { + Context::destroy(self::INSTANCE_CONTEXT_KEY); + } +} From 5e00b0e49976be39b455fc296ba63ff83969865d Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:57:46 +0000 Subject: [PATCH 02/10] Add Onceable class for once() helper Onceable captures caller context (file, line, class, object) to generate unique hashes for memoization. Supports HasOnceHash interface for custom hash implementations. --- src/support/src/Onceable.php | 94 ++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/support/src/Onceable.php diff --git a/src/support/src/Onceable.php b/src/support/src/Onceable.php new file mode 100644 index 000000000..ecd535661 --- /dev/null +++ b/src/support/src/Onceable.php @@ -0,0 +1,94 @@ +> $trace + * @return static|null + */ + public static function tryFromTrace(array $trace, callable $callable) + { + if (! is_null($hash = static::hashFromTrace($trace, $callable))) { + $object = static::objectFromTrace($trace); + + return new static($hash, $object, $callable); + } + } + + /** + * Computes the object of the onceable from the given trace, if any. + * + * @param array> $trace + * @return object|null + */ + protected static function objectFromTrace(array $trace) + { + return $trace[1]['object'] ?? null; + } + + /** + * Computes the hash of the onceable from the given trace. + * + * @param array> $trace + * @return string|null + */ + protected static function hashFromTrace(array $trace, callable $callable) + { + if (str_contains($trace[0]['file'] ?? '', 'eval()\'d code')) { + return null; + } + + $uses = array_map( + static function (mixed $argument) { + if ($argument instanceof HasOnceHash) { + return $argument->onceHash(); + } + + if (is_object($argument)) { + return spl_object_hash($argument); + } + + return $argument; + }, + $callable instanceof Closure ? (new ReflectionClosure($callable))->getClosureUsedVariables() : [], + ); + + $class = $callable instanceof Closure ? (new ReflectionClosure($callable))->getClosureCalledClass()?->getName() : null; + + $class ??= isset($trace[1]['class']) ? $trace[1]['class'] : null; + + return hash('xxh128', sprintf( + '%s@%s%s:%s (%s)', + $trace[0]['file'], + $class ? $class.'@' : '', + $trace[1]['function'], + $trace[0]['line'], + serialize($uses), + )); + } +} From 7baff8d09a9b808a6b16b31e241077fc496618f3 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:58:50 +0000 Subject: [PATCH 03/10] Add once() helper function Memoizes callable results per-coroutine using the Once class. --- src/support/src/helpers.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/support/src/helpers.php b/src/support/src/helpers.php index 1545b5b3c..a4b8d0d4f 100644 --- a/src/support/src/helpers.php +++ b/src/support/src/helpers.php @@ -8,6 +8,8 @@ use Hypervel\Support\Contracts\Htmlable; use Hypervel\Support\Environment; use Hypervel\Support\HigherOrderTapProxy; +use Hypervel\Support\Once; +use Hypervel\Support\Onceable; use Hypervel\Support\Sleep; if (! function_exists('value')) { @@ -219,6 +221,26 @@ function object_get(object $object, ?string $key, mixed $default = null): mixed } } +if (! function_exists('once')) { + /** + * Ensures a callable is only called once, and returns the result on subsequent calls. + * + * @template TReturnType + * + * @param callable(): TReturnType $callback + * @return TReturnType + */ + function once(callable $callback) + { + $onceable = Onceable::tryFromTrace( + debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), + $callback, + ); + + return $onceable ? Once::instance()->value($onceable) : call_user_func($callback); + } +} + if (! function_exists('optional')) { /** * Provide access to optional objects. From 79a47ec4d65a188f3063fcad2300d6327e5cdd1d Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:59:02 +0000 Subject: [PATCH 04/10] Add OnceTest for coroutine-safe memoization Tests verify: - Caching within coroutine - Differentiation based on closure uses - Coroutine isolation (parallel calls get independent results) --- tests/Support/OnceTest.php | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/Support/OnceTest.php diff --git a/tests/Support/OnceTest.php b/tests/Support/OnceTest.php new file mode 100644 index 000000000..3f7a4ffda --- /dev/null +++ b/tests/Support/OnceTest.php @@ -0,0 +1,88 @@ +newCounter(); + + $first = $this->runOnceWithCounter($counter); + $second = $this->runOnceWithCounter($counter); + + $this->assertSame(1, $first); + $this->assertSame(1, $second); + $this->assertSame(1, $counter->value); + } + + public function testOnceDifferentiatesClosureUses(): void + { + $results = array_map( + fn (int $value) => once(fn () => $value), + [1, 2], + ); + + $this->assertSame([1, 2], $results); + } + + public function testOnceIsCoroutineScoped(): void + { + $counter = $this->newCounter(); + $results = []; + + run(function () use (&$results, $counter): void { + $results = parallel([ + fn () => $this->runOnceWithCounter($counter), + fn () => $this->runOnceWithCounter($counter), + ]); + }); + + sort($results); + + $this->assertSame([1, 2], $results); + $this->assertSame(2, $counter->value); + } + + private function newCounter(): object + { + return new class { + public int $value = 0; + }; + } + + private function runOnceWithCounter(object $counter): int + { + return once(function () use ($counter): int { + return ++$counter->value; + }); + } +} From 6487a09af49c8a9ad240d36c96bd13e769a53cde Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:59:12 +0000 Subject: [PATCH 05/10] Add OnceableTest for trace-based hash generation Tests verify: - Caller object capture from trace - HasOnceHash interface support for custom hash implementations --- tests/Support/OnceableTest.php | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/Support/OnceableTest.php diff --git a/tests/Support/OnceableTest.php b/tests/Support/OnceableTest.php new file mode 100644 index 000000000..10c274500 --- /dev/null +++ b/tests/Support/OnceableTest.php @@ -0,0 +1,66 @@ +createOnceable(fn () => 'value'); + + $this->assertSame($this, $onceable->object); + } + + public function testHashUsesOnceHashImplementation(): void + { + $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); + + $value = new OnceHashStub('same'); + $onceableA = Onceable::tryFromTrace($trace, fn () => $value); + + $value = new OnceHashStub('same'); + $onceableB = Onceable::tryFromTrace($trace, fn () => $value); + + $value = new OnceHashStub('different'); + $onceableC = Onceable::tryFromTrace($trace, fn () => $value); + + $this->assertNotNull($onceableA); + $this->assertNotNull($onceableB); + $this->assertNotNull($onceableC); + $this->assertSame($onceableA->hash, $onceableB->hash); + $this->assertNotSame($onceableA->hash, $onceableC->hash); + } + + private function createOnceable(callable $callback): Onceable + { + $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); + + $onceable = Onceable::tryFromTrace($trace, $callback); + + $this->assertNotNull($onceable); + + return $onceable; + } +} + +class OnceHashStub implements HasOnceHash +{ + public function __construct(private string $hash) + { + } + + public function onceHash(): string + { + return $this->hash; + } +} From 0bb09f8f7a760dc4e403eaa89349c31682d1df29 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:01:30 +0000 Subject: [PATCH 06/10] Add HasOnceHash contract for custom once() hashing Allows objects to provide custom hash values for once() memoization by implementing the HasOnceHash interface. --- src/support/src/Contracts/HasOnceHash.php | 13 +++++++++++++ src/support/src/Onceable.php | 2 +- tests/Support/OnceableTest.php | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/support/src/Contracts/HasOnceHash.php diff --git a/src/support/src/Contracts/HasOnceHash.php b/src/support/src/Contracts/HasOnceHash.php new file mode 100644 index 000000000..b236c9362 --- /dev/null +++ b/src/support/src/Contracts/HasOnceHash.php @@ -0,0 +1,13 @@ + Date: Sat, 24 Jan 2026 08:02:35 +0000 Subject: [PATCH 07/10] Fix Onceable::tryFromTrace() missing return statement --- src/support/src/Onceable.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/support/src/Onceable.php b/src/support/src/Onceable.php index b843736c5..ee249b0be 100644 --- a/src/support/src/Onceable.php +++ b/src/support/src/Onceable.php @@ -31,13 +31,15 @@ public function __construct( * @param array> $trace * @return static|null */ - public static function tryFromTrace(array $trace, callable $callable) + public static function tryFromTrace(array $trace, callable $callable): ?static { if (! is_null($hash = static::hashFromTrace($trace, $callable))) { $object = static::objectFromTrace($trace); return new static($hash, $object, $callable); } + + return null; } /** From c93523b2232ee03303920fee04b4356fbb5e537b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:03:34 +0000 Subject: [PATCH 08/10] Fix ProcessInspector phpstan annotation error --- src/horizon/src/ProcessInspector.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/horizon/src/ProcessInspector.php b/src/horizon/src/ProcessInspector.php index b6205b164..a03965aea 100644 --- a/src/horizon/src/ProcessInspector.php +++ b/src/horizon/src/ProcessInspector.php @@ -49,7 +49,6 @@ public function monitoring(): array ->pluck('pid') ->pipe(function (Collection $processes) { foreach ($processes as $process) { - /** @var string $process */ $processes = $processes->merge($this->exec->run('pgrep -P ' . (string) $process)); } From 7c3431db8f1f758afcf2e702478c0b6eaf04c6e3 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:04:13 +0000 Subject: [PATCH 09/10] Apply php-cs-fixer formatting --- src/support/src/Once.php | 3 +-- src/support/src/Onceable.php | 18 +++++++----------- src/support/src/helpers.php | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/support/src/Once.php b/src/support/src/Once.php index a1d95c944..511f5106a 100644 --- a/src/support/src/Once.php +++ b/src/support/src/Once.php @@ -22,11 +22,10 @@ class Once /** * Create a new once instance. * - * @param \WeakMap> $values + * @param WeakMap> $values */ protected function __construct(protected WeakMap $values) { - // } /** diff --git a/src/support/src/Onceable.php b/src/support/src/Onceable.php index ee249b0be..d270d8e93 100644 --- a/src/support/src/Onceable.php +++ b/src/support/src/Onceable.php @@ -13,23 +13,19 @@ class Onceable /** * Create a new onceable instance. * - * @param string $hash - * @param object|null $object - * @param callable $callable + * @param callable $callable */ public function __construct( public string $hash, public ?object $object, public $callable, ) { - // } /** * Tries to create a new onceable instance from the given trace. * - * @param array> $trace - * @return static|null + * @param array> $trace */ public static function tryFromTrace(array $trace, callable $callable): ?static { @@ -45,8 +41,8 @@ public static function tryFromTrace(array $trace, callable $callable): ?static /** * Computes the object of the onceable from the given trace, if any. * - * @param array> $trace - * @return object|null + * @param array> $trace + * @return null|object */ protected static function objectFromTrace(array $trace) { @@ -56,8 +52,8 @@ protected static function objectFromTrace(array $trace) /** * Computes the hash of the onceable from the given trace. * - * @param array> $trace - * @return string|null + * @param array> $trace + * @return null|string */ protected static function hashFromTrace(array $trace, callable $callable) { @@ -87,7 +83,7 @@ static function (mixed $argument) { return hash('xxh128', sprintf( '%s@%s%s:%s (%s)', $trace[0]['file'], - $class ? $class.'@' : '', + $class ? $class . '@' : '', $trace[1]['function'], $trace[0]['line'], serialize($uses), diff --git a/src/support/src/helpers.php b/src/support/src/helpers.php index a4b8d0d4f..5651de293 100644 --- a/src/support/src/helpers.php +++ b/src/support/src/helpers.php @@ -227,7 +227,7 @@ function object_get(object $object, ?string $key, mixed $default = null): mixed * * @template TReturnType * - * @param callable(): TReturnType $callback + * @param callable(): TReturnType $callback * @return TReturnType */ function once(callable $callback) From ed6904bc5bda9abdd4dca185815e11c6003ecd47 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:19:19 +0000 Subject: [PATCH 10/10] Fix duplicate type in ManagesFrequencies docblock --- src/console/src/Scheduling/ManagesFrequencies.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/console/src/Scheduling/ManagesFrequencies.php b/src/console/src/Scheduling/ManagesFrequencies.php index 8c3c3c34f..a163153a4 100644 --- a/src/console/src/Scheduling/ManagesFrequencies.php +++ b/src/console/src/Scheduling/ManagesFrequencies.php @@ -499,8 +499,6 @@ public function yearly(): static /** * Schedule the event to run yearly on a given month, day, and time. - * - * @param int|string|string $dayOfMonth */ public function yearlyOn(int $month = 1, int|string $dayOfMonth = 1, string $time = '0:0'): static {