├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── CacheItem.php ├── CacheKey.php ├── InMemoryCache.php └── InvalidArgument.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Unreleased 4 | 5 | ## 1.3.1 - 2024-08-26 6 | 7 | * Made clock argument of `InMemoryCache` constructor explicitly nullable 8 | ([#4](https://github.com/beste/in-memory-cache-php/pull/4)) 9 | 10 | ## 1.3.0 - 2024-08-17 11 | 12 | * Added support for PHP 8.4 13 | 14 | ## 1.2.0 - 2024-07-27 15 | 16 | * The [PSR-6 definition on what makes a valid cache key](https://www.php-fig.org/psr/psr-6/#definitions), it is said that 17 | keys must support keys consisting of the characters `A-Z`, `a-z`, `0-9`, `_`, and `.` in any order in UTF-8 18 | encoding and a length of up to 64 characters. Implementing libraries MAY support additional characters and encodings 19 | or longer lengths, but must support at least that minimum. 20 | * Dashes (`-`) are now allowed in cache keys. 21 | * The arbitrary maximum key length of 64 characters has been removed. 22 | 23 | 24 | ## 1.1.0 - 2024-03-02 25 | 26 | * The Cache can now be instantiated without providing a [PSR-20](https://www.php-fig.org/psr/psr-20/) clock implementation. 27 | * The library doesn't depend on the [`beste/clock` library](https://github.com/beste/clock) anymore. 28 | 29 | ## 1.0.0 - 2023-12-09 30 | 31 | Initial Release 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jérôme Gamez, https://github.com/beste 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-6 In-Memory Cache 2 | 3 | A [PSR-6](https://www.php-fig.org/psr/psr-6/) In-Memory cache that can be used as a default implementation and in tests. 4 | 5 | [![Current version](https://img.shields.io/packagist/v/beste/in-memory-cache.svg?logo=composer)](https://packagist.org/packages/beste/in-memory-cache) 6 | [![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/beste/in-memory-cache)](https://packagist.org/packages/beste/in-memory-cache) 7 | [![Monthly Downloads](https://img.shields.io/packagist/dm/beste/in-memory-cache.svg)](https://packagist.org/packages/beste/in-memory-cache/stats) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/beste/in-memory-cache.svg)](https://packagist.org/packages/beste/in-memory-cache/stats) 9 | [![Tests](https://github.com/beste/in-memory-cache-php/actions/workflows/tests.yml/badge.svg)](https://github.com/beste/in-memory-cache-php/actions/workflows/tests.yml) 10 | 11 | ## Installation 12 | 13 | ```shell 14 | composer require beste/in-memory-cache 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```php 20 | use Beste\Cache\InMemoryCache; 21 | 22 | $cache = new InMemoryCache(); 23 | 24 | $item = $cache->getItem('key'); 25 | 26 | assert($item->isHit() === false); 27 | assert($item->get() === null); 28 | 29 | $item->set('value'); 30 | $cache->save($item); 31 | 32 | // Later... 33 | 34 | $item = $cache->getItem('key'); 35 | 36 | assert($item->isHit() === true); 37 | assert($item->get() === 'value'); 38 | ``` 39 | 40 | You can also provide your own [PSR-20](https://www.php-fig.org/psr/psr-20/) clock implementation, for example a frozen 41 | clock for testing, for example from the [`beste/clock` library](https://github.com/beste/clock). 42 | 43 | ```php 44 | use Beste\Clock\FrozenClock; 45 | use Beste\Cache\InMemoryCache; 46 | 47 | $clock = FrozenClock::fromUTC() 48 | $cache = new InMemoryCache(); 49 | 50 | $item = $cache->getItem('key'); 51 | $item->set('value')->expiresAfter(new DateInterval('PT5M')); 52 | $cache->save($item); 53 | 54 | $clock->setTo($clock->now()->add(new DateInterval('PT2M'))); 55 | assert($cache->getItem('key')->isHit() === true); 56 | 57 | $clock->setTo($clock->now()->add(new DateInterval('PT5M'))); 58 | assert($cache->getItem('key')->isHit() === false); 59 | ``` 60 | 61 | ## Running tests 62 | 63 | ```shell 64 | composer test 65 | ``` 66 | 67 | ## License 68 | 69 | This project is published under the [MIT License](LICENSE). 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beste/in-memory-cache", 3 | "description": "A PSR-6 In-Memory cache that can be used as a fallback implementation and/or in tests.", 4 | "keywords": ["cache", "psr-6", "beste"], 5 | "license": "MIT", 6 | "type": "library", 7 | "authors": [ 8 | { 9 | "name": "Jérôme Gamez", 10 | "email": "jerome@gamez.name" 11 | } 12 | ], 13 | "require": { 14 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 15 | "psr/cache": "^2.0 || ^3.0", 16 | "psr/clock": "^1.0" 17 | }, 18 | "require-dev": { 19 | "beste/clock": "^3.0", 20 | "beste/php-cs-fixer-config": "^3.2.0", 21 | "friendsofphp/php-cs-fixer": "^3.62.0", 22 | "phpstan/extension-installer": "^1.4.1", 23 | "phpstan/phpstan": "^2.0.1", 24 | "phpstan/phpstan-deprecation-rules": "^2.0", 25 | "phpstan/phpstan-phpunit": "^2.0", 26 | "phpstan/phpstan-strict-rules": "^2.0", 27 | "phpunit/phpunit": "^10.5.2 || ^11.3.1", 28 | "symfony/var-dumper": "^6.4 || ^7.1.3" 29 | }, 30 | "provide": { 31 | "psr/cache-implementation": "2.0 || 3.0" 32 | }, 33 | "suggest": { 34 | "psr/clock-implementation": "Allows injecting a Clock, for example a frozen clock for testing" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Beste\\Cache\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Beste\\Cache\\Tests\\": "tests" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "phpstan/extension-installer": true 49 | }, 50 | "sort-packages": true 51 | }, 52 | "scripts": { 53 | "analyse": "vendor/bin/phpstan analyse", 54 | "analyze": "@analyse", 55 | "cs-fix": "vendor/bin/php-cs-fixer fix --diff --verbose", 56 | "test": "vendor/bin/phpunit --testdox", 57 | "test-coverage": [ 58 | "Composer\\Config::disableProcessTimeout", 59 | "XDEBUG_MODE=coverage vendor/bin/phpunit --testdox --coverage-html=.build/coverage" 60 | ], 61 | "check": [ 62 | "@cs-fix", 63 | "@analyse", 64 | "@test" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/CacheItem.php: -------------------------------------------------------------------------------- 1 | value = null; 20 | $this->expiresAt = null; 21 | $this->isHit = false; 22 | } 23 | 24 | public function getKey(): string 25 | { 26 | return $this->key->toString(); 27 | } 28 | 29 | public function get(): mixed 30 | { 31 | if ($this->isHit()) { 32 | return $this->value; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | public function isHit(): bool 39 | { 40 | if ($this->isHit === false) { 41 | return false; 42 | } 43 | 44 | if ($this->expiresAt === null) { 45 | return true; 46 | } 47 | 48 | return $this->clock->now()->getTimestamp() < $this->expiresAt->getTimestamp(); 49 | } 50 | 51 | public function set(mixed $value): static 52 | { 53 | $this->isHit = true; 54 | $this->value = $value; 55 | 56 | return $this; 57 | } 58 | 59 | public function expiresAt(?\DateTimeInterface $expiration): static 60 | { 61 | $this->expiresAt = $expiration; 62 | 63 | return $this; 64 | } 65 | 66 | public function expiresAfter(\DateInterval|int|null $time): static 67 | { 68 | if ($time === null) { 69 | $this->expiresAt = null; 70 | return $this; 71 | } 72 | 73 | if (is_int($time)) { 74 | $time = new \DateInterval("PT{$time}S"); 75 | } 76 | 77 | $this->expiresAt = $this->clock->now()->add($time); 78 | 79 | return $this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/CacheKey.php: -------------------------------------------------------------------------------- 1 | value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/InMemoryCache.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $items; 16 | /** @var array */ 17 | private array $deferredItems; 18 | 19 | public function __construct( 20 | ?ClockInterface $clock = null, 21 | ) { 22 | $this->clock = $clock ?? new class implements ClockInterface { 23 | public function now(): DateTimeImmutable 24 | { 25 | return new DateTimeImmutable(); 26 | } 27 | 28 | }; 29 | $this->items = []; 30 | $this->deferredItems = []; 31 | } 32 | 33 | public function getItem(string $key): CacheItemInterface 34 | { 35 | $key = CacheKey::fromString($key); 36 | 37 | $item = $this->items[$key->toString()] ?? null; 38 | 39 | if ($item === null) { 40 | return new CacheItem($key, $this->clock); 41 | } 42 | 43 | return clone $item; 44 | } 45 | 46 | /** 47 | * @return iterable 48 | */ 49 | public function getItems(array $keys = []): iterable 50 | { 51 | if ($keys === []) { 52 | return []; 53 | } 54 | 55 | $items = []; 56 | 57 | foreach ($keys as $key) { 58 | $items[$key] = $this->getItem($key); 59 | } 60 | 61 | return $items; 62 | } 63 | 64 | public function hasItem(string $key): bool 65 | { 66 | return $this->getItem($key)->isHit(); 67 | } 68 | 69 | public function clear(): bool 70 | { 71 | $this->items = []; 72 | $this->deferredItems = []; 73 | 74 | return true; 75 | } 76 | 77 | public function deleteItem(string $key): bool 78 | { 79 | $key = CacheKey::fromString($key); 80 | 81 | unset($this->items[$key->toString()]); 82 | 83 | return true; 84 | } 85 | 86 | public function deleteItems(array $keys): bool 87 | { 88 | foreach ($keys as $key) { 89 | $this->deleteItem($key); 90 | } 91 | 92 | return true; 93 | } 94 | 95 | public function save(CacheItemInterface $item): bool 96 | { 97 | $this->items[$item->getKey()] = $item; 98 | 99 | return true; 100 | } 101 | 102 | public function saveDeferred(CacheItemInterface $item): bool 103 | { 104 | $this->deferredItems[$item->getKey()] = $item; 105 | 106 | return true; 107 | } 108 | 109 | public function commit(): bool 110 | { 111 | foreach ($this->deferredItems as $item) { 112 | $this->save($item); 113 | } 114 | 115 | $this->deferredItems = []; 116 | 117 | return true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/InvalidArgument.php: -------------------------------------------------------------------------------- 1 |