Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CachedMap #19

Merged
merged 1 commit into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
100 changes: 100 additions & 0 deletions src/CachedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Marvin255\InMemoryCache;

/**
* Map functionality for cached items.
*
* @internal
*
* @implements \Iterator<string, CachedItem>
*/
final class CachedMap implements \Countable, \Iterator
{
/**
* @var array<string, CachedItem>
*/
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);
}
}
53 changes: 23 additions & 30 deletions src/InMemoryCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CachedItem>
*/
private array $stack = [];
private readonly ClockInterface $clock;

public function __construct(
private readonly int $stackSize = self::DEFAULT_STACK_SIZE,
Expand All @@ -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();
}

Expand All @@ -43,24 +41,22 @@ 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;
}

/**
* {@inheritDoc}
*/
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;
}
Expand All @@ -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;
}
Expand All @@ -80,7 +76,7 @@ public function delete(string $key): bool
*/
public function clear(): bool
{
$this->stack = [];
$this->cachedMap->clear();

return true;
}
Expand Down Expand Up @@ -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)
);
}

/**
Expand All @@ -149,46 +147,41 @@ 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;
}
}

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();
}
}
Loading