From 47d09a9cd722ed68bf341e5e2eace6abb8604737 Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Sun, 4 Feb 2024 17:13:09 +0800 Subject: [PATCH] Adds `once` memoization function (#550) * Adds once memoization function * Delete test-results cache file * Add .phpunit.cache to .gitignore * Add null return statement in Onceable::create() method --------- Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- src/Functions.php | 18 +++++++++ src/Once.php | 98 +++++++++++++++++++++++++++++++++++++++++++++++ src/Onceable.php | 85 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/Once.php create mode 100644 src/Onceable.php diff --git a/src/Functions.php b/src/Functions.php index 6917a06..7202613 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -58,3 +58,21 @@ function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) goto beginning; } } + +/** + * 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); +} diff --git a/src/Once.php b/src/Once.php new file mode 100644 index 0000000..cbd5136 --- /dev/null +++ b/src/Once.php @@ -0,0 +1,98 @@ +> $values + */ + protected function __construct(protected WeakMap $values) + { + } + + /** + * Create a new once instance. + * + * @return static + */ + public static function instance() + { + return static::$instance ??= new static(new WeakMap()); + } + + /** + * Get the value of the given onceable. + * + * @return mixed + */ + public function value(Onceable $onceable) + { + if (! static::$enabled) { + return call_user_func($onceable->callable); + } + + $object = $onceable->object ?: $this; + + $hash = $onceable->hash; + + if (isset($this->values[$object][$hash])) { + return $this->values[$object][$hash]; + } + + if (! isset($this->values[$object])) { + $this->values[$object] = []; + } + + return $this->values[$object][$hash] = call_user_func($onceable->callable); + } + + /** + * Re-enable the once instance if it was disabled. + */ + public static function enable() + { + static::$enabled = true; + } + + /** + * Disable the once instance. + */ + public static function disable() + { + static::$enabled = false; + } + + /** + * Flush the once instance. + */ + public static function flush() + { + static::$instance = null; + } +} diff --git a/src/Onceable.php b/src/Onceable.php new file mode 100644 index 0000000..734ab8e --- /dev/null +++ b/src/Onceable.php @@ -0,0 +1,85 @@ +> $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); + } + + return null; + } + + /** + * 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( + fn (mixed $argument) => is_object($argument) ? spl_object_hash($argument) : $argument, + $callable instanceof Closure ? (new ReflectionClosure($callable))->getClosureUsedVariables() : [], + ); + + return md5(sprintf( + '%s@%s%s:%s (%s)', + $trace[0]['file'], + isset($trace[1]['class']) ? ($trace[1]['class'] . '@') : '', + $trace[1]['function'], + $trace[0]['line'], + serialize($uses), + )); + } +}