diff --git a/src/CachedServiceGenerator/Dto/MethodCallObject.php b/src/CachedServiceGenerator/Dto/MethodCallObject.php index 5fee730..aa0725e 100644 --- a/src/CachedServiceGenerator/Dto/MethodCallObject.php +++ b/src/CachedServiceGenerator/Dto/MethodCallObject.php @@ -4,6 +4,8 @@ namespace Tbessenreither\MultiLevelCache\CachedServiceGenerator\Dto; +use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\KeyGeneratorService; + class MethodCallObject { public function __construct( @@ -41,7 +43,7 @@ public function getAdditionalCacheKey(): ?string public function getCachePrefix(): string { - return str_replace(['\\', ':', ' '], ['_', '-', ''], $this->getClass()); + return KeyGeneratorService::namespaceToKeyString($this->getClass()); } public function getCallable(): callable diff --git a/src/CachedServiceGenerator/Service/InvalidatorService.php b/src/CachedServiceGenerator/Service/InvalidatorService.php index 1054c54..678f166 100644 --- a/src/CachedServiceGenerator/Service/InvalidatorService.php +++ b/src/CachedServiceGenerator/Service/InvalidatorService.php @@ -19,25 +19,47 @@ public function __construct( $this->directRedisCacheService = $multiLevelCacheFactory->getImplementationRedisWithPrefix('mlc'); } - public function invalidateCacheForClass(string $classString): bool + public function invalidateEverything(): bool { - try { - $generateKey = KeyGeneratorService::getKeyPatternForClass(new MethodCallObject($classString, '', [])); + return $this->deleteByPattern('*'); + } - $this->directRedisCacheService->deleteByPattern($generateKey); + public function invalidateCacheForNamespace(string $namespace): bool + { + $completedWithoutError = true; + $invalidateKeyPatterns = [ + KeyGeneratorService::namespaceToKeyString($namespace, null, true) . ':*', + KeyGeneratorService::namespaceToKeyString($namespace, null, false) . ':*', + ]; + foreach ($invalidateKeyPatterns as $pattern) { - return true; - } catch (Throwable $e) { - return false; + $result = $this->deleteByPattern($pattern); + if ($result === false) { + $completedWithoutError = false; + } } + + return $completedWithoutError; + } + + public function invalidateCacheForClass(string $classString): bool + { + $generateKey = KeyGeneratorService::getKeyPatternForClass(new MethodCallObject($classString, '', [])); + + return $this->deleteByPattern($generateKey); } public function invalidateCacheForMethod(string $classString, string $method): bool { - try { - $generateKey = KeyGeneratorService::getKeyPatternForMethod(new MethodCallObject($classString, $method, [])); + $generateKey = KeyGeneratorService::getKeyPatternForMethod(new MethodCallObject($classString, $method, [])); - $this->directRedisCacheService->deleteByPattern($generateKey); + return $this->deleteByPattern($generateKey); + } + + public function deleteByPattern(string $pattern): bool + { + try { + $this->directRedisCacheService->deleteByPattern($pattern); return true; } catch (Throwable $e) { diff --git a/src/CachedServiceGenerator/Service/KeyGeneratorService.php b/src/CachedServiceGenerator/Service/KeyGeneratorService.php index 232cf76..657d1ab 100644 --- a/src/CachedServiceGenerator/Service/KeyGeneratorService.php +++ b/src/CachedServiceGenerator/Service/KeyGeneratorService.php @@ -14,6 +14,8 @@ class KeyGeneratorService { + public const string CACHED_SERVICE_GENERATOR_KEY_PREFIX = 'CachedService'; + /** * @var array */ @@ -28,6 +30,20 @@ public static function getKey(MethodCallObject $methodCallObject, bool $throw = return self::defaultKeyGenerator($methodCallObject); } + public static function namespaceToKeyString(string $fqcn, ?string $method = null, bool $addCsgPrefix = false): string + { + $parts = []; + if ($addCsgPrefix) { + $parts[] = self::CACHED_SERVICE_GENERATOR_KEY_PREFIX; + } + $parts[] = str_replace(['\\', ':', ' '], ['_', '-', ''], $fqcn); + if ($method) { + $parts[] = $method; + } + + return implode(':', $parts); + } + public static function getKeyPatternForMethod(MethodCallObject $methodCallObject): string { $generatedFullKey = self::getKey($methodCallObject); diff --git a/src/CachedServiceGenerator/Service/MakeCachedServiceService.php b/src/CachedServiceGenerator/Service/MakeCachedServiceService.php index 385e839..70bbd77 100644 --- a/src/CachedServiceGenerator/Service/MakeCachedServiceService.php +++ b/src/CachedServiceGenerator/Service/MakeCachedServiceService.php @@ -129,6 +129,7 @@ class: $interfaceClass, $classCode = RenderTemplateService::render('Class/CachedService', [ 'ServiceNamespace' => $namespace, 'ServiceName' => $shortName, + 'CacheKeyPrefix' => KeyGeneratorService::namespaceToKeyString(fqcn:$class, addCsgPrefix:true), 'ClassDotSeparated' => $classDotSeparated, 'ClassUnderscoreSeparated' => $classUnderscoreSeparated, 'ClassHyphenSeparated' => $classHyphenSeparated, diff --git a/src/CachedServiceGenerator/Service/MlcTemplates/Class/CachedServiceTemplate.txt b/src/CachedServiceGenerator/Service/MlcTemplates/Class/CachedServiceTemplate.txt index 87afafe..da62e9f 100644 --- a/src/CachedServiceGenerator/Service/MlcTemplates/Class/CachedServiceTemplate.txt +++ b/src/CachedServiceGenerator/Service/MlcTemplates/Class/CachedServiceTemplate.txt @@ -26,10 +26,11 @@ use Throwable; * * Cache enabled wrapper for Service /*{ServiceName}*/. * - * This Wrapper provides all public methods from the origin service and was generated using the make command - * You can regenerate/update using php vendor/tbessenreither/multi-level-cache/bin/make --service=/*{ClassDotSeparated}*/ - * Or you can update all services with php vendor/tbessenreither/multi-level-cache/bin/update + * This Wrapper provides all public methods from the origin service and was generated using the make command. + * You can regenerate/update using php vendor/tbessenreither/multi-level-cache/bin/make --service=/*{ClassDotSeparated}*/. + * Or you can update all services with php vendor/tbessenreither/multi-level-cache/bin/update. * @codeCoverageIgnore + * @mixin /*{ServiceName}*/ */ #[MlcCachedService( originalServiceClass: /*{ServiceName}*/::class, @@ -37,7 +38,7 @@ use Throwable; )] class /*{ServiceName}*/Cached implements /*{InterfacesString}*/ { - public const CACHE_KEY_PREFIX = 'CachedService:/*{ClassUnderscoreSeparated}*/'; + public const CACHE_KEY_PREFIX = '/*{CacheKeyPrefix}*/'; /** * @var array diff --git a/tests/CachedServiceGenerator/Dto/MethodCallObjectTest.php b/tests/CachedServiceGenerator/Dto/MethodCallObjectTest.php index 86fc805..7fdb9e7 100644 --- a/tests/CachedServiceGenerator/Dto/MethodCallObjectTest.php +++ b/tests/CachedServiceGenerator/Dto/MethodCallObjectTest.php @@ -6,10 +6,13 @@ use Closure; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Dto\MethodCallObject; +use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\KeyGeneratorService; #[CoversClass(MethodCallObject::class)] +#[UsesClass(KeyGeneratorService::class)] class MethodCallObjectTest extends TestCase { public function testSetupAndGetter(): void diff --git a/tests/CachedServiceGenerator/Service/InvalidatorServiceTest.php b/tests/CachedServiceGenerator/Service/InvalidatorServiceTest.php index 1fe6237..68c7f5a 100644 --- a/tests/CachedServiceGenerator/Service/InvalidatorServiceTest.php +++ b/tests/CachedServiceGenerator/Service/InvalidatorServiceTest.php @@ -31,6 +31,53 @@ #[UsesClass(RedisClientFactory::class)] class InvalidatorServiceTest extends TestCase { + public function testInvalidateEverything(): void + { + $redisMock = $this->createMock(Redis::class); + $redisMock->method('isConnected')->willReturn(true); + $redisMock->method('scan')->willReturnOnConsecutiveCalls( + ['key:1', 'key:2'], + [] + ); + $redisMock + ->expects($this->atLeastOnce()) + ->method('del'); + + $directRedisCacheService = new DirectRedisCacheService(redisClientProvider: RedisClientFactoryTest::wrapClient($redisMock)); + + $multiLevelCacheFactoryStub = $this->createStub(MultiLevelCacheFactory::class); + $multiLevelCacheFactoryStub->method('getImplementationRedisWithPrefix') + ->willReturn($directRedisCacheService); + + $invalidatorService = new InvalidatorService($multiLevelCacheFactoryStub); + $result = $invalidatorService->invalidateEverything(); + $this->assertTrue($result); + } + + public function testInvalidateEverythingWithError(): void + { + $redisMock = $this->createMock(Redis::class); + $redisMock->method('isConnected')->willReturn(true); + $redisMock->method('scan')->willReturnOnConsecutiveCalls( + ['key:1', 'key:2'], + [] + ); + $redisMock + ->expects($this->atLeastOnce()) + ->method('del') + ->willThrowException(new Exception('random error')); + + $directRedisCacheService = new DirectRedisCacheService(redisClientProvider: RedisClientFactoryTest::wrapClient($redisMock)); + + $multiLevelCacheFactoryStub = $this->createStub(MultiLevelCacheFactory::class); + $multiLevelCacheFactoryStub->method('getImplementationRedisWithPrefix') + ->willReturn($directRedisCacheService); + + $invalidatorService = new InvalidatorService($multiLevelCacheFactoryStub); + $result = $invalidatorService->invalidateEverything(); + $this->assertFalse($result); + } + public function testClassInvalidation(): void { $redisStub = $this->createStub(Redis::class); @@ -100,4 +147,47 @@ public function testMethodInvalidationError(): void $result = $invalidatorService->invalidateCacheForMethod(TestServiceA::class, 'testMethod'); $this->assertFalse($result); } + + public function testNamespaceInvalidation(): void + { + $redisMock = $this->createMock(Redis::class); + $redisMock->method('isConnected')->willReturn(true); + $redisMock->method('scan')->willReturnOnConsecutiveCalls( + ['key:1', 'key:2'], + [] + ); + $redisMock->expects($this->atLeastOnce()) + ->method('del'); + $directRedisCacheService = new DirectRedisCacheService(redisClientProvider: RedisClientFactoryTest::wrapClient($redisMock)); + + $multiLevelCacheFactoryStub = $this->createStub(MultiLevelCacheFactory::class); + $multiLevelCacheFactoryStub->method('getImplementationRedisWithPrefix') + ->willReturn($directRedisCacheService); + + $invalidatorService = new InvalidatorService($multiLevelCacheFactoryStub); + $result = $invalidatorService->invalidateCacheForNamespace(TestServiceA::class); + $this->assertTrue($result); + } + + public function testNamespaceInvalidationWithError(): void + { + $redisMock = $this->createMock(Redis::class); + $redisMock->method('isConnected')->willReturn(true); + $redisMock->method('scan')->willReturnOnConsecutiveCalls( + ['key:1', 'key:2'], + [] + ); + $redisMock->expects($this->atLeastOnce()) + ->method('del') + ->willThrowException(new Exception('random error')); + $directRedisCacheService = new DirectRedisCacheService(redisClientProvider: RedisClientFactoryTest::wrapClient($redisMock)); + + $multiLevelCacheFactoryStub = $this->createStub(MultiLevelCacheFactory::class); + $multiLevelCacheFactoryStub->method('getImplementationRedisWithPrefix') + ->willReturn($directRedisCacheService); + + $invalidatorService = new InvalidatorService($multiLevelCacheFactoryStub); + $result = $invalidatorService->invalidateCacheForNamespace(TestServiceA::class); + $this->assertFalse($result); + } } diff --git a/tests/Commands/MakeTest.php b/tests/Commands/MakeTest.php index d460a82..c8ce622 100644 --- a/tests/Commands/MakeTest.php +++ b/tests/Commands/MakeTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; +use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\KeyGeneratorService; #[CoversNothing] class MakeTest extends TestCase @@ -69,6 +70,7 @@ public function testMake(): void $this->assertStringContainsString('public function exampleMethod(): void', $interfaceFileContent); $this->assertStringContainsString('public function exampleMethodWithArguments(array $thing, bool $bool, int $int, string $string): void', $interfaceFileContent); + $this->assertStringContainsString("CACHE_KEY_PREFIX = '" . KeyGeneratorService::CACHED_SERVICE_GENERATOR_KEY_PREFIX . ":", $cachedFileContent); } public function testMakeWithInterfaces(): void