From 2dac84d1a42da7fc86d2a4b906d2ab78b6012508 Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Mon, 27 May 2024 17:37:10 +0800 Subject: [PATCH] Bumps `once` to v3 (#646) Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- src/Functions.php | 41 +++++++++++++----- src/Once.php | 98 ------------------------------------------ src/Once/Backtrace.php | 74 +++++++++++++++++++++++++++++++ src/Once/Cache.php | 97 +++++++++++++++++++++++++++++++++++++++++ src/Onceable.php | 85 ------------------------------------ 5 files changed, 202 insertions(+), 193 deletions(-) delete mode 100644 src/Once.php create mode 100644 src/Once/Backtrace.php create mode 100644 src/Once/Cache.php delete mode 100644 src/Onceable.php diff --git a/src/Functions.php b/src/Functions.php index 617c75a..8a95522 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -60,19 +60,40 @@ function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) } /** - * Ensures a callable is only called once, and returns the result on subsequent calls. + * @template T * - * @template TReturnType - * - * @param callable(): TReturnType $callback - * @return TReturnType + * @param (callable(): T) $callback + * @return T */ -function once(callable $callback) +function once(callable $callback): mixed { - $onceable = Onceable::tryFromTrace( - debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), - $callback, + $trace = debug_backtrace( + DEBUG_BACKTRACE_PROVIDE_OBJECT, + 2 ); - return $onceable ? Once::instance()->value($onceable) : call_user_func($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); } diff --git a/src/Once.php b/src/Once.php deleted file mode 100644 index cbd5136..0000000 --- a/src/Once.php +++ /dev/null @@ -1,98 +0,0 @@ -> $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/Once/Backtrace.php b/src/Once/Backtrace.php new file mode 100644 index 0000000..fada239 --- /dev/null +++ b/src/Once/Backtrace.php @@ -0,0 +1,74 @@ +trace = $trace[1]; + + $this->zeroStack = $trace[0]; + } + + public function getArguments(): array + { + return $this->trace['args']; + } + + public function getFunctionName(): string + { + return $this->trace['function']; + } + + public function getObjectName(): ?string + { + return $this->trace['class'] ?? null; + } + + public function getObject(): mixed + { + if ($this->globalFunction()) { + return $this->zeroStack['file']; + } + + return $this->staticCall() ? $this->trace['class'] : $this->trace['object']; + } + + public function getHash(): string + { + $normalizedArguments = array_map(function ($argument) { + return is_object($argument) ? spl_object_hash($argument) : $argument; + }, $this->getArguments()); + + $prefix = $this->getObjectName() . $this->getFunctionName(); + if (str_contains($prefix, '{closure')) { + $prefix = $this->zeroStack['line']; + } + + return md5($prefix . serialize($normalizedArguments)); + } + + protected function staticCall(): bool + { + return $this->trace['type'] === '::'; + } + + protected function globalFunction(): bool + { + return ! isset($this->trace['type']); + } +} diff --git a/src/Once/Cache.php b/src/Once/Cache.php new file mode 100644 index 0000000..15b2aad --- /dev/null +++ b/src/Once/Cache.php @@ -0,0 +1,97 @@ +values = new WeakMap(); + } + + public static function getInstance(): static + { + return static::$cache ??= new static(); + } + + public function has(object $object, string $backtraceHash): bool + { + if (! isset($this->values[$object])) { + return false; + } + + return array_key_exists($backtraceHash, $this->values[$object]); + } + + public function get($object, string $backtraceHash): mixed + { + return $this->values[$object][$backtraceHash]; + } + + public function set(object $object, string $backtraceHash, mixed $value): void + { + $cached = $this->values[$object] ?? []; + + $cached[$backtraceHash] = $value; + + $this->values[$object] = $cached; + } + + public function forget(object $object): void + { + unset($this->values[$object]); + } + + public function flush(): self + { + $this->values = new WeakMap(); + + return $this; + } + + public function enable(): self + { + $this->enabled = true; + + return $this; + } + + public function disable(): self + { + $this->enabled = false; + + return $this; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function count(): int + { + return count($this->values); + } +} diff --git a/src/Onceable.php b/src/Onceable.php deleted file mode 100644 index 61b035e..0000000 --- a/src/Onceable.php +++ /dev/null @@ -1,85 +0,0 @@ -> $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), - )); - } -}