├── .editorconfig ├── .github └── workflows │ └── quality-assurance.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── BasicCacheItemAccessorsTrait.php ├── BasicCacheItemTrait.php ├── BasicPoolTrait.php ├── CacheException.php ├── CachePoolDeferTrait.php ├── InvalidArgumentException.php ├── KeyValidatorTrait.php └── Memory │ ├── MemoryCacheItem.php │ └── MemoryPool.php ├── test.php └── test ├── CachePoolDeferTraitTest.php ├── KeyValidatorTest.php └── MemoryPoolTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # PHP should follow the PSR-2 standard (https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) 13 | [*.php] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.github/workflows/quality-assurance.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Quality Assurance 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | 7 | jobs: 8 | phpunit: 9 | name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | php: [ '8.0', '8.1', '8.2', '8.3' ] 14 | composer-flags: [ '' ] 15 | phpunit-flags: [ '--coverage-text' ] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php }} 21 | coverage: xdebug 22 | tools: composer:v2 23 | - run: composer update --no-progress ${{ matrix.composer-flags }} 24 | - run: vendor/bin/phpunit ${{ matrix.phpunit-flags }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.0.0 - 2016-08-06 6 | 7 | ### Fixed 8 | 9 | Initial stable release 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2016 the PHP Framework Interoperability Group 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-6 Cache Utilities 2 | 3 | ![Quality Assurance](https://github.com/php-fig/cache-util/workflows/Quality%20Assurance/badge.svg) 4 | 5 | This package contains a series of traits and base classes to cover the common, 6 | boilerplate portions of implementing a PSR-6-compliant cache library. 7 | 8 | This package also provides a basic in-memory-only PSR-6 implementation. While 9 | not useful for actual use, it serves as a demo-implementation as well as a 10 | debugging tool. 11 | 12 | This package does not, and will not, provide a complete production-ready PSR-6 13 | implementation. If you are looking for a PSR-6 implementation, consult your 14 | friendly local Packagist.org server. 15 | 16 | ## License 17 | 18 | This package is released under the MIT license. See LICENSE for details. 19 | 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fig/cache-util", 3 | "description": "Useful utility classes and traits for implementing the PSR cache standard", 4 | "keywords": ["psr", "psr-6", "cache"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "PHP-FIG", 9 | "homepage": "http://www.php-fig.org/" 10 | }, 11 | { 12 | "name": "Larry Garfield", 13 | "homepage": "http://www.garfieldtech.com/" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.0.0", 18 | "psr/cache": "^2.0 | ^3.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^9.5", 22 | "squizlabs/php_codesniffer": "^2.3.1" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Fig\\Cache\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Fig\\Cache\\Test\\": "test/" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "2.0.x-dev" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR-2 coding standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src 20 | test 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./test 5 | 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/BasicCacheItemAccessorsTrait.php: -------------------------------------------------------------------------------- 1 | expiration ?? new \DateTime('now +1 year'); 30 | } 31 | 32 | /** 33 | * Returns the raw value, regardless of hit status. 34 | * 35 | * Although not part of the CacheItemInterface, this method is used by 36 | * the pool for extracting information for saving. 37 | * 38 | * @return mixed 39 | * 40 | * @internal 41 | */ 42 | public function getRawValue(): mixed 43 | { 44 | return $this->value; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/BasicCacheItemTrait.php: -------------------------------------------------------------------------------- 1 | key; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function get(): mixed 30 | { 31 | return $this->isHit() ? $this->value : null; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function set($value = null): static 38 | { 39 | $this->value = $value; 40 | return $this; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function isHit(): bool 47 | { 48 | return $this->hit; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function expiresAt(?\DateTimeInterface $expiration): static 55 | { 56 | $this->expiration = $expiration ?? new \DateTimeImmutable('now +1 year'); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function expiresAfter(int|\DateInterval|null $time): static 65 | { 66 | $this->expiration = match(true) { 67 | is_null($time) => new \DateTimeImmutable('now +1 year'), 68 | is_int($time) => new \DateTimeImmutable('now +' . $time . ' seconds'), 69 | $time instanceof \DateInterval => (new \DateTimeImmutable())->add($time), 70 | }; 71 | return $this; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/BasicPoolTrait.php: -------------------------------------------------------------------------------- 1 | deleteItems([$key]); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | abstract public function deleteItems(array $items): bool; 25 | } 26 | -------------------------------------------------------------------------------- /src/CacheException.php: -------------------------------------------------------------------------------- 1 | write([$item]); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function saveDeferred(CacheItemInterface $item): bool 32 | { 33 | $this->deferred[] = $item; 34 | return true; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function commit(): bool 41 | { 42 | $success = $this->write($this->deferred); 43 | if ($success) { 44 | $this->deferred = []; 45 | } 46 | return $success; 47 | } 48 | 49 | /** 50 | * Commits the specified cache items to storage. 51 | * 52 | * @param CacheItemInterface[] $items 53 | * 54 | * @return bool 55 | * TRUE if all provided items were successfully saved. FALSE otherwise. 56 | */ 57 | abstract protected function write(array $items): bool; 58 | } 59 | -------------------------------------------------------------------------------- /src/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | getReservedKeyCharacters()) . ']#', $key); 41 | if ($unsupportedMatched > 0) { 42 | throw new InvalidArgumentException('Can\'t validate the specified key'); 43 | } 44 | 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Memory/MemoryCacheItem.php: -------------------------------------------------------------------------------- 1 | key = $key; 28 | $this->value = $data['value']; 29 | $this->expiration = $data['ttd']; 30 | $this->hit = $data['hit']; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Memory/MemoryPool.php: -------------------------------------------------------------------------------- 1 | validateKey($key); 35 | 36 | if (!$this->hasItem($key)) { 37 | $this->data[$key] = $this->emptyItem(); 38 | } 39 | 40 | return new MemoryCacheItem($key, $this->data[$key]); 41 | } 42 | 43 | /** 44 | * Returns an empty item definition. 45 | * 46 | * @return array 47 | */ 48 | protected function emptyItem(): array 49 | { 50 | return [ 51 | 'value' => null, 52 | 'hit' => false, 53 | 'ttd' => null, 54 | ]; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getItems(array $keys = []): iterable 61 | { 62 | // This method will throw an appropriate exception if any key is not valid. 63 | array_map([$this, 'validateKey'], $keys); 64 | 65 | $collection = []; 66 | foreach ($keys as $key) { 67 | $collection[$key] = $this->getItem($key); 68 | } 69 | return $collection; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function clear(): bool 76 | { 77 | $this->data = []; 78 | return true; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function deleteItems(array $keys): bool 85 | { 86 | foreach ($keys as $key) { 87 | unset($this->data[$key]); 88 | } 89 | return true; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function hasItem($key): bool 96 | { 97 | return array_key_exists($key, $this->data) && $this->data[$key]['ttd'] > new \DateTimeImmutable(); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | protected function write(array $items): bool 104 | { 105 | /** @var \Psr\Cache\CacheItemInterface $item */ 106 | foreach ($items as $item) { 107 | $this->data[$item->getKey()] = [ 108 | // Assumes use of the BasicCacheItemAccessorsTrait. 109 | 'value' => $item->getRawValue(), 110 | 'ttd' => $item->getExpiration(), 111 | 'hit' => true, 112 | ]; 113 | } 114 | 115 | return true; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | getItem('foo'); 16 | $item->set('foo value', '300'); 17 | $pool->save($item); 18 | $item = $pool->getItem('bar'); 19 | $item->set('bar value', new \DateTime('now + 5min')); 20 | $pool->save($item); 21 | 22 | foreach ($pool->getItems(['foo', 'bar']) as $key => $item) { 23 | if ($key == 'foo') { 24 | assert($item->get() == 'foo value'); 25 | } 26 | if ($key == 'bar') { 27 | assert($item->get() == 'bar value'); 28 | } 29 | } 30 | 31 | // Update an existing item. 32 | $items = $pool->getItems(['foo', 'bar']); 33 | $items['bar']->set('new bar value'); 34 | array_map([$pool, 'save'], $items); 35 | 36 | foreach ($pool->getItems(['foo', 'bar']) as $item) { 37 | if ($item->getKey() == 'foo') { 38 | assert($item->get() == 'foo value'); 39 | } 40 | if ($item->getKey() == 'bar') { 41 | assert($item->get() == 'new bar value'); 42 | } 43 | } 44 | 45 | // Defer saving to a later operation. 46 | $item = $pool->getItem('baz')->set('baz value', '100'); 47 | $pool->saveDeferred($item); 48 | $item = $pool->getItem('foo')->set('new foo value', new \DateTime('now + 1min')); 49 | $pool->saveDeferred($item); 50 | $pool->commit(); 51 | 52 | $items = $pool->getItems(['foo', 'bar', 'baz']); 53 | assert($items['foo']->get() == 'new foo value'); 54 | assert($items['bar']->get() == 'new bar value'); 55 | assert($items['baz']->get() == 'baz value'); 56 | -------------------------------------------------------------------------------- /test/CachePoolDeferTraitTest.php: -------------------------------------------------------------------------------- 1 | traitStub = $this->getMockForTrait(CachePoolDeferTrait::class); 17 | $this->itemStub = $this->createMock(CacheItemInterface::class); 18 | } 19 | 20 | public function testSaveSuccess(): void 21 | { 22 | $this->traitStub->expects(static::once()) 23 | ->method('write') 24 | ->with(static::equalTo([$this->itemStub])) 25 | ->willReturn(true); 26 | static::assertTrue($this->traitStub->save($this->itemStub)); 27 | } 28 | 29 | public function testSaveFail(): void 30 | { 31 | $this->traitStub->expects(static::once()) 32 | ->method('write') 33 | ->willReturn(false); 34 | static::assertFalse($this->traitStub->save($this->itemStub)); 35 | } 36 | 37 | public function testSaveDeferred(): void 38 | { 39 | static::assertTrue($this->traitStub->saveDeferred($this->itemStub)); 40 | } 41 | 42 | public function testCommitSuccess(): void 43 | { 44 | $otherItem = clone $this->itemStub; 45 | $this->traitStub->expects(static::once()) 46 | ->method('write') 47 | ->with(static::equalTo([$this->itemStub, $otherItem])) 48 | ->willReturn(true); 49 | 50 | $this->traitStub->saveDeferred($this->itemStub); 51 | $this->traitStub->saveDeferred($otherItem); 52 | static::assertTrue($this->traitStub->commit()); 53 | } 54 | 55 | public function testCommitFail(): void 56 | { 57 | $this->traitStub->expects(static::once()) 58 | ->method('write') 59 | ->willReturn(false); 60 | 61 | $this->traitStub->saveDeferred($this->itemStub); 62 | static::assertFalse($this->traitStub->commit()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/KeyValidatorTest.php: -------------------------------------------------------------------------------- 1 | pool = new MemoryPool(); 18 | } 19 | 20 | /** 21 | * Verifies key's name in positive cases. 22 | * 23 | * @param string $key 24 | * The key's name. 25 | * 26 | * @dataProvider providerValidKeyNames 27 | */ 28 | public function testPositiveValidateKey($key) 29 | { 30 | static::assertInstanceOf(MemoryCacheItem::class, $this->pool->getItem($key)); 31 | } 32 | 33 | /** 34 | * Provides a set of valid test key names. 35 | * 36 | * @return array 37 | */ 38 | public function providerValidKeyNames(): array 39 | { 40 | return [ 41 | ['bar'], 42 | ['barFoo1234567890'], 43 | ['bar_Foo.1'], 44 | ['1'], 45 | [str_repeat('a', 64)] 46 | ]; 47 | } 48 | 49 | /** 50 | * Verifies key's name in negative cases. 51 | * 52 | * @param string $key 53 | * The key's name. 54 | * 55 | * @expectedException InvalidArgumentException 56 | * @dataProvider providerNotValidKeyNames 57 | */ 58 | public function testNegativeValidateKey($key): void 59 | { 60 | $this->expectException(InvalidArgumentException::class); 61 | $this->pool->getItem($key); 62 | } 63 | 64 | /** 65 | * Provides a set of not valid test key names. 66 | * 67 | * @return array 68 | */ 69 | public function providerNotValidKeyNames(): array 70 | { 71 | return [ 72 | [null], 73 | [1], 74 | [''], 75 | ['bar{Foo'], 76 | ['bar}Foo'], 77 | ['bar(Foo'], 78 | ['bar)Foo'], 79 | ['bar/Foo'], 80 | ['bar\Foo'], 81 | ['bar@Foo'], 82 | ['bar:Foo'] 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/MemoryPoolTest.php: -------------------------------------------------------------------------------- 1 | hasItem('foo')); 19 | 20 | $item = $pool->getItem('foo'); 21 | 22 | static::assertNull($item->get()); 23 | static::assertFalse($item->isHit()); 24 | } 25 | 26 | /** 27 | * Verifies that primitive items can be added and retrieved from the pool. 28 | * 29 | * @param mixed $value 30 | * A value to try and cache. 31 | * @param string $type 32 | * The type of variable we expect to be cached. 33 | * 34 | * @dataProvider providerPrimitiveValues 35 | */ 36 | public function testAddItem($value, $type): void 37 | { 38 | $pool = new MemoryPool(); 39 | 40 | $item = $pool->getItem('foo'); 41 | $item->set($value); 42 | $pool->save($item); 43 | 44 | $item = $pool->getItem('foo'); 45 | static::assertEquals($value, $item->get()); 46 | static::assertEquals($type, gettype($item->get())); 47 | } 48 | 49 | /** 50 | * Provides a set of test values for saving and retrieving. 51 | * 52 | * @return array 53 | */ 54 | public function providerPrimitiveValues(): array 55 | { 56 | return [ 57 | ['bar', 'string'], 58 | [1, 'integer'], 59 | [3.141592, 'double'], 60 | [['a', 'b', 'c'], 'array'], 61 | [['a' => 'A', 'b' => 'B', 'c' => 'C'], 'array'], 62 | ]; 63 | } 64 | 65 | /** 66 | * Verifies that an item with an expiration time in the past won't be retrieved. 67 | * 68 | * @param mixed $value 69 | * A value to try and cache. 70 | * 71 | * @dataProvider providerPrimitiveValues 72 | */ 73 | public function testExpiresAt($value): void 74 | { 75 | $pool = new MemoryPool(); 76 | 77 | $item = $pool->getItem('foo'); 78 | $item 79 | ->set($value) 80 | ->expiresAt(new \DateTimeImmutable('-1 minute')); 81 | $pool->save($item); 82 | 83 | $item = $pool->getItem('foo'); 84 | static::assertNull($item->get()); 85 | static::assertFalse($item->isHit()); 86 | } 87 | } 88 | --------------------------------------------------------------------------------