From 951593ab3a95864a6d99efee6cfb6fd69efe4faa Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Fri, 20 Dec 2024 17:05:10 +0800 Subject: [PATCH] feat: introduce Once and Onceable classes, deprecate Cache and Backtrace (#808) * feat: introduce Once and Onceable classes, deprecate Cache and Backtrace * test: add PHPUnit group attribute to OnceTest class * fix: return null when no valid instance is created in Onceable class --------- Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- src/Functions.php | 41 +++++------------- src/Once.php | 98 ++++++++++++++++++++++++++++++++++++++++++ src/Once/Backtrace.php | 3 ++ src/Once/Cache.php | 3 ++ src/Onceable.php | 85 ++++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 src/Once.php create mode 100644 src/Onceable.php diff --git a/src/Functions.php b/src/Functions.php index f3d09e2..5656a98 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -61,40 +61,19 @@ function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) } /** - * @template TReturn + * Ensures a callable is only called once, and returns the result on subsequent calls. + * + * @template TReturnType * - * @param (callable(): TReturn) $callback - * @return TReturn + * @param callable(): TReturnType $callback + * @return TReturnType */ -function once(callable $callback): mixed +function once(callable $callback) { - $trace = debug_backtrace( - DEBUG_BACKTRACE_PROVIDE_OBJECT, - 2 + $onceable = Onceable::tryFromTrace( + debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), + $callback, ); - $backtrace = new Once\Backtrace($trace); - - if ($backtrace->getFunctionName() === 'eval') { - return call_user_func($callback); - } - - $object = $backtrace->getObject(); - $hash = $backtrace->getHash(); - $cache = Once\Cache::getInstance(); - - if (is_string($object)) { - $object = $cache; - } - - if (! $cache->isEnabled()) { - return call_user_func($callback, $backtrace->getArguments()); - } - - if (! $cache->has($object, $hash)) { - $result = call_user_func($callback, $backtrace->getArguments()); - $cache->set($object, $hash, $result); - } - - return $cache->get($object, $hash); + 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..e2eb19e --- /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])) { + $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() + { + 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/Once/Backtrace.php b/src/Once/Backtrace.php index fada239..7d53c34 100644 --- a/src/Once/Backtrace.php +++ b/src/Once/Backtrace.php @@ -11,6 +11,9 @@ namespace FriendsOfHyperf\Support\Once; +/** + * @deprecated since v3.1, use FriendsOfHyperf\Support\Onceable instead, will removed in v3.2 + */ class Backtrace { protected array $trace; diff --git a/src/Once/Cache.php b/src/Once/Cache.php index 15b2aad..1328298 100644 --- a/src/Once/Cache.php +++ b/src/Once/Cache.php @@ -14,6 +14,9 @@ use Countable; use WeakMap; +/** + * @deprecated since v3.1, use FriendsOfHyperf\Support\Once instead, will removed in v3.2 + */ class Cache implements Countable { public WeakMap $values; diff --git a/src/Onceable.php b/src/Onceable.php new file mode 100644 index 0000000..55a548c --- /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), + )); + } +}