From 5a28646d14d5176ff3e3f1ac15cf9aa8842916d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Glawaty?= Date: Mon, 5 Feb 2024 03:19:30 +0100 Subject: [PATCH] Added cache implementation for queries --- composer.json | 1 + .../infrastructure.cache.neon | 9 ++ .../DI/definitions/message_bus/query_bus.neon | 1 + .../Middleware/QueryCacheMiddleware.php | 86 +++++++++++++ .../Messenger/Stamp/RefreshCacheStamp.php | 11 ++ .../Infrastructure/Cache/CacheInterface.php | 30 +++++ .../Infrastructure/Cache/CacheMetadata.php | 19 +++ .../Cache/UnableToDeleteCacheException.php | 11 ++ .../Cache/UnableToReadCacheException.php | 11 ++ .../Cache/UnableToWriteCacheException.php | 11 ++ .../DoctrinePersistenceAdapter.php | 2 +- .../Infrastructure/NetteCache/NetteCache.php | 113 ++++++++++++++++++ .../Query/CachableQueryInterface.php | 12 ++ 13 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 src/ArchitectureBundle/Bridge/Nette/DI/definitions/architecture_bundle/infrastructure.cache.neon create mode 100644 src/ArchitectureBundle/Bridge/Symfony/Messenger/Middleware/QueryCacheMiddleware.php create mode 100644 src/ArchitectureBundle/Bridge/Symfony/Messenger/Stamp/RefreshCacheStamp.php create mode 100644 src/ArchitectureBundle/Infrastructure/Cache/CacheInterface.php create mode 100644 src/ArchitectureBundle/Infrastructure/Cache/CacheMetadata.php create mode 100644 src/ArchitectureBundle/Infrastructure/Cache/UnableToDeleteCacheException.php create mode 100644 src/ArchitectureBundle/Infrastructure/Cache/UnableToReadCacheException.php create mode 100644 src/ArchitectureBundle/Infrastructure/Cache/UnableToWriteCacheException.php create mode 100644 src/ArchitectureBundle/Infrastructure/NetteCache/NetteCache.php create mode 100644 src/ArchitectureBundle/ReadModel/Query/CachableQueryInterface.php diff --git a/composer.json b/composer.json index 9dea0d4..f2699d5 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "kubawerlos/php-cs-fixer-custom-fixers": "^3.13", "latte/latte": "^3.0", "nette/application": "^3.1", + "nette/caching": "^3.2", "nette/mail": "^3.1", "nette/security": "^3.1", "nette/tester": "^2.5", diff --git a/src/ArchitectureBundle/Bridge/Nette/DI/definitions/architecture_bundle/infrastructure.cache.neon b/src/ArchitectureBundle/Bridge/Nette/DI/definitions/architecture_bundle/infrastructure.cache.neon new file mode 100644 index 0000000..d38e256 --- /dev/null +++ b/src/ArchitectureBundle/Bridge/Nette/DI/definitions/architecture_bundle/infrastructure.cache.neon @@ -0,0 +1,9 @@ +services: + infrastructure.cache: + autowired: SixtyEightPublishers\ArchitectureBundle\Infrastructure\Cache\CacheInterface + type: SixtyEightPublishers\ArchitectureBundle\Infrastructure\Cache\CacheInterface + factory: @extension.infrastructure.cache.nette + + infrastructure.cache.nette: + autowired: no + factory: SixtyEightPublishers\ArchitectureBundle\Infrastructure\NetteCache\NetteCache diff --git a/src/ArchitectureBundle/Bridge/Nette/DI/definitions/message_bus/query_bus.neon b/src/ArchitectureBundle/Bridge/Nette/DI/definitions/message_bus/query_bus.neon index 94cd65f..833b480 100644 --- a/src/ArchitectureBundle/Bridge/Nette/DI/definitions/message_bus/query_bus.neon +++ b/src/ArchitectureBundle/Bridge/Nette/DI/definitions/message_bus/query_bus.neon @@ -4,4 +4,5 @@ middleware: - Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware(query_bus) - Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware - SixtyEightPublishers\ArchitectureBundle\Bridge\Symfony\Messenger\Middleware\OriginalExceptionMiddleware + - SixtyEightPublishers\ArchitectureBundle\Bridge\Symfony\Messenger\Middleware\QueryCacheMiddleware panel: %debugMode% diff --git a/src/ArchitectureBundle/Bridge/Symfony/Messenger/Middleware/QueryCacheMiddleware.php b/src/ArchitectureBundle/Bridge/Symfony/Messenger/Middleware/QueryCacheMiddleware.php new file mode 100644 index 0000000..25dd147 --- /dev/null +++ b/src/ArchitectureBundle/Bridge/Symfony/Messenger/Middleware/QueryCacheMiddleware.php @@ -0,0 +1,86 @@ +getMessage(); + + if (!($message instanceof CachableQueryInterface)) { + return $stack->next()->handle( + envelope: $envelope, + stack: $stack, + ); + } + + $refreshCache = $envelope->last(RefreshCacheStamp::class) !== null; + $cacheMetadata = $message->createCacheMetadata(); + + try { + $item = !$refreshCache ? $this->cache->getItem( + key: $cacheMetadata->key, + ) : null; + } catch (UnableToReadCacheException $e) { + if (null === $this->logger) { + throw $e; + } + + $this->logger->error( + message: $e->getMessage(), + context: [ + 'exception' => $e, + ], + ); + + $item = null; + } + + if (null !== $item) { + return $item; + } + + $item = $stack->next()->handle( + envelope: $envelope, + stack: $stack, + ); + + try { + $this->cache->saveItem( + metadata: $cacheMetadata, + item: $item, + ); + } catch (UnableToWriteCacheException $e) { + if (null === $this->logger) { + throw $e; + } + + $this->logger->error( + message: $e->getMessage(), + context: [ + 'exception' => $e, + ], + ); + } + + return $item; + } +} diff --git a/src/ArchitectureBundle/Bridge/Symfony/Messenger/Stamp/RefreshCacheStamp.php b/src/ArchitectureBundle/Bridge/Symfony/Messenger/Stamp/RefreshCacheStamp.php new file mode 100644 index 0000000..dec29a4 --- /dev/null +++ b/src/ArchitectureBundle/Bridge/Symfony/Messenger/Stamp/RefreshCacheStamp.php @@ -0,0 +1,11 @@ +|null $tags + * + * @throws UnableToDeleteCacheException + */ + public function clean(?array $tags = null): void; +} diff --git a/src/ArchitectureBundle/Infrastructure/Cache/CacheMetadata.php b/src/ArchitectureBundle/Infrastructure/Cache/CacheMetadata.php new file mode 100644 index 0000000..57d04ff --- /dev/null +++ b/src/ArchitectureBundle/Infrastructure/Cache/CacheMetadata.php @@ -0,0 +1,19 @@ + $tags + */ + public function __construct( + public readonly string $key, + public readonly int|DateTimeImmutable|null $expiration, + public readonly array $tags = [], + ) {} +} diff --git a/src/ArchitectureBundle/Infrastructure/Cache/UnableToDeleteCacheException.php b/src/ArchitectureBundle/Infrastructure/Cache/UnableToDeleteCacheException.php new file mode 100644 index 0000000..c53ea0f --- /dev/null +++ b/src/ArchitectureBundle/Infrastructure/Cache/UnableToDeleteCacheException.php @@ -0,0 +1,11 @@ +executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); - } catch (DBALException $e) { + } catch (DbalException $e) { $connection->close(); $connection->connect(); } diff --git a/src/ArchitectureBundle/Infrastructure/NetteCache/NetteCache.php b/src/ArchitectureBundle/Infrastructure/NetteCache/NetteCache.php new file mode 100644 index 0000000..abdfb9a --- /dev/null +++ b/src/ArchitectureBundle/Infrastructure/NetteCache/NetteCache.php @@ -0,0 +1,113 @@ +cache = new Cache( + storage: $storage, + namespace: self::class, + ); + } + + public function getItem(string $key): mixed + { + try { + return $this->cache->load( + key: $key, + ); + } catch (Throwable $e) { + throw new UnableToReadCacheException( + message: $e->getMessage(), + code: $e->getCode(), + previous: $e, + ); + } + } + + public function saveItem(CacheMetadata $metadata, mixed $item): void + { + $dependencies = []; + + if ($metadata->expiration instanceof DateTimeImmutable) { + $dependencies[Cache::Expire] = $metadata->expiration->format('U.u'); + } elseif (is_int($metadata->expiration)) { + $dependencies[Cache::Expire] = $metadata->expiration + time(); + } + + if (0 < count($metadata->tags)) { + $dependencies[Cache::Tags] = $metadata->tags; + } + + try { + $this->cache->save( + key: $metadata->key, + data: $item, + dependencies: $dependencies, + ); + } catch (Throwable $e) { + throw new UnableToWriteCacheException( + message: $e->getMessage(), + code: $e->getCode(), + previous: $e, + ); + } + } + + public function deleteItem(string $key): void + { + try { + $this->cache->remove( + key: $key, + ); + } catch (Throwable $e) { + throw new UnableToDeleteCacheException( + message: $e->getMessage(), + code: $e->getCode(), + previous: $e, + ); + } + } + + public function clean(?array $tags = null): void + { + $conditions = null !== $tags + ? [ + Cache::Tags => $tags, + ] + : [ + Cache::All => true, + ]; + + try { + $this->cache->clean( + conditions: $conditions, + ); + } catch (Throwable $e) { + throw new UnableToDeleteCacheException( + message: $e->getMessage(), + code: $e->getCode(), + previous: $e, + ); + } + } +} diff --git a/src/ArchitectureBundle/ReadModel/Query/CachableQueryInterface.php b/src/ArchitectureBundle/ReadModel/Query/CachableQueryInterface.php new file mode 100644 index 0000000..79bb342 --- /dev/null +++ b/src/ArchitectureBundle/ReadModel/Query/CachableQueryInterface.php @@ -0,0 +1,12 @@ +