diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 49281a4..e169661 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -26,7 +26,7 @@ jobs: compiler: jit steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install libmemcached run: sudo apt-get install -y libmemcached-dev @@ -48,7 +48,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} diff --git a/library/iFixit/Matryoshka/Scope.php b/library/iFixit/Matryoshka/Scope.php index 045d347..bbea6b2 100644 --- a/library/iFixit/Matryoshka/Scope.php +++ b/library/iFixit/Matryoshka/Scope.php @@ -21,13 +21,17 @@ public function getPrefix() { return $this->scopePrefix ?: $this->getScopePrefix(); } - public function getScopePrefix(bool $reset = false) { + public function getScopePrefix(bool $reset = false, bool $generateOnMiss = true) { if ($this->scopePrefix === null || $reset) { - $scopeValue = $this->backend->getAndSet($this->getScopeKey(), - function() { - return substr(md5(microtime() . $this->scopeName), 0, 16); - }, 0, $reset); - + $scopeValue = $reset ? self::MISS : $this->backend->get($this->getScopeKey()); + if ($scopeValue === self::MISS) { + if ($generateOnMiss) { + $scopeValue = substr(md5(microtime() . $this->scopeName), 0, 16); + $this->backend->set($this->getScopeKey(), $scopeValue); + } else { + return self::MISS; + } + } $this->scopePrefix = "{$scopeValue}-"; } @@ -52,4 +56,20 @@ public function deleteScope(): bool { private function getScopeKey() { return "scope-{$this->scopeName}"; } + + public function get($key) { + // If the scope prefix doesn't exist, all keys in this scope are a miss. + if (!$this->getScopePrefix(generateOnMiss: false)) { + return self::MISS; + } + return parent::get($key); + } + + public function getMultiple(array $keys) { + // If the scope prefix doesn't exist, all keys in this scope are a miss. + if (!$this->getScopePrefix(generateOnMiss: false)) { + return [array_fill_keys(array_keys($keys), self::MISS), $keys]; + } + return parent::getMultiple($keys); + } } diff --git a/tests/KeyFixTest.php b/tests/KeyFixTest.php index d47c78a..dcc5998 100644 --- a/tests/KeyFixTest.php +++ b/tests/KeyFixTest.php @@ -43,20 +43,14 @@ public function testKeyShorten() { $this->assertSame($cachedValues, $memoryCache->getCache()); } - public function testValidation() { - try { - new Matryoshka\KeyFix(new TestEphemeral(), 5, Memcached::INVALID_CHARS_REGEX); - $this->fail("Doesn't complain about bad regex"); - } catch (Throwable $e) { - // Do nothing. - } + public function testValidationInvalidCharsRegex() { + $this->expectException(InvalidArgumentException::class); + new Matryoshka\KeyFix(new TestEphemeral(), 5, Memcached::INVALID_CHARS_REGEX); + } - try { - new Matryoshka\KeyFix(new TestEphemeral(), 40, ''); - $this->fail("Doesn't complain about bad regex"); - } catch (Throwable $e) { - // Do nothing. - } + public function testValidationBlankRegex() { + $this->expectWarning(); + new Matryoshka\KeyFix(new TestEphemeral(), 40, ''); } public function testNoBadChars() { diff --git a/tests/KeyShortenTest.php b/tests/KeyShortenTest.php index c621904..06db098 100644 --- a/tests/KeyShortenTest.php +++ b/tests/KeyShortenTest.php @@ -46,12 +46,8 @@ public function testKeyShorten() { } public function testKeyShortenLength() { - try { - new Matryoshka\KeyShorten(new TestEphemeral(), 5); - $this->fail("Doesn't throw InvalidArgumentException"); - } catch (InvalidArgumentException $e) { - // Do nothing. - } + $this->expectException(InvalidArgumentException::class); + new Matryoshka\KeyShorten(new TestEphemeral(), 5); } public function testAbsoluteKey() { diff --git a/tests/MultiScopeTest.php b/tests/MultiScopeTest.php index bdcb30d..244f47f 100644 --- a/tests/MultiScopeTest.php +++ b/tests/MultiScopeTest.php @@ -16,12 +16,8 @@ protected function getBackend() { } public function testMultiScopeBadArgument() { - try { - new Matryoshka\MultiScope(new Matryoshka\Ephemeral(), ['string']); - $this->fail("Doesn't throw InvalidArgumentException"); - } catch (InvalidArgumentException $e) { - // Do nothing. - } + $this->expectException(InvalidArgumentException::class); + new Matryoshka\MultiScope(new Matryoshka\Ephemeral(), ['string']); } public function testMultiScope() { diff --git a/tests/ScopeTest.php b/tests/ScopeTest.php index 9313956..49bb109 100644 --- a/tests/ScopeTest.php +++ b/tests/ScopeTest.php @@ -12,8 +12,7 @@ protected function getBackend() { public function testScope() { $memoryCache = new Matryoshka\Ephemeral(); $scope = 'scope'; - $scopedCache = new Matryoshka\Scope($memoryCache, - $scope); + $scopedCache = new Matryoshka\Scope($memoryCache, $scope); list($key1, $value1) = $this->getRandomKeyValue(); list($key2, $value2) = $this->getRandomKeyValue(); @@ -41,6 +40,54 @@ public function testScope() { $this->assertTrue($scopedCache->set($key1, $value1)); $this->assertSame($value1, $scopedCache->get($key1)); + + $scopedCache = new Matryoshka\Scope($memoryCache, $scope); + $this->assertSame($value1, $scopedCache->get($key1)); + } + + public function testScopeShortCircuitGet() { + $scope = 'scope'; + $mockBackend = $this->getMockBuilder(Matryoshka\Ephemeral::class) + ->setMethods(['get']) + ->getMock(); + + // Assert get() is called only once because a missing scope value means + // that all underlying values are also missing. + $mockBackend->expects($this->once()) + ->method('get') + ->with($this->stringContains($scope)) + ->willReturn(Matryoshka\Backend::MISS); + $scopedCache = new Matryoshka\Scope($mockBackend, $scope); + + $key = (string)rand(1, 100000); + $this->assertNull($scopedCache->get($key)); + } + + public function testScopeShortCircuitGetMultiple() { + $scope = 'scope'; + $mockBackend = $this->getMockBuilder(Matryoshka\Ephemeral::class) + ->setMethods(['get', 'getMultiple']) + ->getMock(); + + // Assert get() is called only once because a missing scope value means + // that all underlying values are also missing. + $mockBackend->expects($this->once()) + ->method('get') + ->with($this->stringContains($scope)) + ->willReturn(Matryoshka\Backend::MISS); + // Assert getMultiple() is never called because a missi + // we don't need to check individual keys. + $mockBackend->expects($this->never()) + ->method('getMultiple'); + + $scopedCache = new Matryoshka\Scope($mockBackend, $scope); + + $key = (string)rand(1, 100000); + $keys = [$key => 'no matter']; + $this->assertSame( + [[$key => Matryoshka\Backend::MISS], $keys], + $scopedCache->getMultiple($keys) + ); } public function testAbsoluteKey() {