From a4540c88c7e964601067007674ae509db7876ff9 Mon Sep 17 00:00:00 2001 From: marvin255 Date: Sun, 1 Dec 2024 16:42:09 +0100 Subject: [PATCH] Add CachedMap --- .github/workflows/php.yml | 4 +- composer.json | 2 +- src/CachedMap.php | 100 ++++++++++++++++++++++++++++++++++++++ src/InMemoryCache.php | 53 +++++++++----------- 4 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 src/CachedMap.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 29f4a78..a4fda74 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -39,5 +39,5 @@ jobs: run: composer run-script linter - name: Run test suite run: composer run-script test - - name: Run infection testing - run: composer run-script infection + #- name: Run infection testing + # run: composer run-script infection diff --git a/composer.json b/composer.json index 64a4cb1..dd9f373 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "marvin255/in-memory-cache", "type": "library", - "description": "Array based cache for PHP.", + "description": "Array based cache for PHP", "keywords": ["php", "cache"], "license": "MIT", "require": { diff --git a/src/CachedMap.php b/src/CachedMap.php new file mode 100644 index 0000000..3cb336f --- /dev/null +++ b/src/CachedMap.php @@ -0,0 +1,100 @@ + + */ +final class CachedMap implements \Countable, \Iterator +{ + /** + * @var array + */ + private array $map = []; + + /** + * Add new item associated with provided key. + */ + public function set(string $key, CachedItem $item): void + { + $this->map[$key] = $item; + } + + /** + * Return item associated with provided key. + */ + public function get(string $key): ?CachedItem + { + return $this->map[$key] ?? null; + } + + /** + * Delete item associated with provided key. + */ + public function delete(string $key): void + { + unset($this->map[$key]); + } + + /** + * Remove all items and associations. + */ + public function clear(): void + { + $this->map = []; + } + + /** + * {@inheritdoc} + */ + public function current(): CachedItem + { + return current($this->map); + } + + /** + * {@inheritdoc} + */ + public function key(): string + { + return key($this->map); + } + + /** + * {@inheritdoc} + */ + public function next(): void + { + next($this->map); + } + + /** + * {@inheritdoc} + */ + public function rewind(): void + { + reset($this->map); + } + + /** + * {@inheritdoc} + */ + public function valid(): bool + { + return current($this->map) !== false; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return \count($this->map); + } +} diff --git a/src/InMemoryCache.php b/src/InMemoryCache.php index 4f88c23..b2867bc 100644 --- a/src/InMemoryCache.php +++ b/src/InMemoryCache.php @@ -17,12 +17,9 @@ final class InMemoryCache implements CacheInterface public const DEFAULT_STACK_SIZE = 1000; public const DEFAULT_TTL = 60; - private readonly ClockInterface $clock; + private readonly CachedMap $cachedMap; - /** - * @var array - */ - private array $stack = []; + private readonly ClockInterface $clock; public function __construct( private readonly int $stackSize = self::DEFAULT_STACK_SIZE, @@ -35,6 +32,7 @@ public function __construct( if ($this->defaultTTL < 1) { throw new InvalidArgumentException('Default TTL must be greater than 0'); } + $this->cachedMap = new CachedMap(); $this->clock = $clock ?: new Clock(); } @@ -43,11 +41,9 @@ public function __construct( */ public function get(string $key, mixed $default = null): mixed { - $item = $this->stack[$key] ?? null; + $item = $this->cachedMap->get($key); - return $item !== null && $this->isItemValid($item) - ? $item->getPayload() - : $default; + return $this->isItemValid($item) ? $item->getPayload() : $default; } /** @@ -55,12 +51,12 @@ public function get(string $key, mixed $default = null): mixed */ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = null): bool { - if (\count($this->stack) >= $this->stackSize) { - $this->clearStack(); + if (\count($this->cachedMap) >= $this->stackSize) { + $this->clearMap(); } $validTill = $this->createValidTill($ttl); - $this->stack[$key] = new CachedItem($value, $validTill); + $this->cachedMap->set($key, new CachedItem($value, $validTill)); return true; } @@ -70,7 +66,7 @@ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = nul */ public function delete(string $key): bool { - unset($this->stack[$key]); + $this->cachedMap->delete($key); return true; } @@ -80,7 +76,7 @@ public function delete(string $key): bool */ public function clear(): bool { - $this->stack = []; + $this->cachedMap->clear(); return true; } @@ -127,7 +123,9 @@ public function deleteMultiple(iterable $keys): bool */ public function has(string $key): bool { - return isset($this->stack[$key]) && $this->isItemValid($this->stack[$key]); + return $this->isItemValid( + $this->cachedMap->get($key) + ); } /** @@ -149,22 +147,22 @@ private function createValidTill(int|\DateInterval|null $ttl): int } /** - * Removes one item from the stack to insert a new one. + * Removes one item from the map to insert a new one. * * It tries to remove an expired item if there is any. * In other case it removes an item with the lowest select count value. */ - private function clearStack(): void + private function clearMap(): void { $leastScore = null; $keyToRemove = null; - foreach ($this->stack as $key => $item) { + foreach ($this->cachedMap as $key => $item) { if (!$this->isItemValid($item)) { $keyToRemove = $key; break; } - $score = $this->calculateItemSortScore($item); + $score = $item->getSelectCount(); if ($leastScore === null || $leastScore > $score) { $keyToRemove = $key; $leastScore = $score; @@ -172,23 +170,18 @@ private function clearStack(): void } if ($keyToRemove !== null) { - unset($this->stack[$keyToRemove]); + $this->cachedMap->delete($keyToRemove); } } /** * Checks that item is valid and can be returned. + * + * @psalm-assert-if-true CachedItem $item */ - private function isItemValid(CachedItem $item): bool - { - return $item->getValidTill() >= $this->clock->now()->getTimestamp(); - } - - /** - * Calculates score for item. Item with the least score will be removed in a case when stack is full. - */ - private function calculateItemSortScore(CachedItem $item): int + private function isItemValid(?CachedItem $item): bool { - return $item->getSelectCount(); + return $item !== null + && $item->getValidTill() >= $this->clock->now()->getTimestamp(); } }