From a4f6de4fe4fd2f8bb3d93d33e22d0f94b29ff9b5 Mon Sep 17 00:00:00 2001 From: Paula Date: Fri, 3 Jul 2026 11:55:58 +0200 Subject: [PATCH] feat!: introduce storage abstraction layer Core services no longer depend on the Elasticsearch bridge. New ActivityLogStorageInterface, CurrentDataTrackerStorageInterface and StorageInitializerInterface in Locastic\Loggastic\Storage decouple the processor, providers, message handlers and console commands from the backend; the default Elasticsearch implementations live in Bridge\Elasticsearch\Storage and are wired via interface aliases. Also moves ElasticNormalizationContextTrait to Serializer\Traits\NormalizationContextTrait, replaces ActivityLogProviderInterface::getActivityLogsByIndexAndId() with ElasticsearchActivityLogStorage::findByIndexAndObjectId(), and removes the broken ActivityLogProvider::getCurrentDataTrackerByClassAndId() and the unused IndexNotFoundException. --- CHANGELOG.md | 9 ++ README.md | 25 +++++- UPGRADE-2.0.md | 32 +++++++ config/elastic.yaml | 19 ++++ config/logger.yaml | 4 +- config/message_handlers.yaml | 3 +- phpstan-baseline.neon | 48 ---------- .../Elasticsearch/ElasticsearchService.php | 4 +- .../ElasticsearchActivityLogStorage.php | 88 +++++++++++++++++++ ...ElasticsearchCurrentDataTrackerStorage.php | 59 +++++++++++++ .../ElasticsearchStorageInitializer.php | 54 ++++++++++++ src/Command/CreateLoggableIndexesCommand.php | 25 ++---- .../PopulateCurrentDataTrackersCommand.php | 6 +- src/DataProcessor/ActivityLogProcessor.php | 27 +++--- src/DataProvider/ActivityLogProvider.php | 84 ++---------------- .../ActivityLogProviderInterface.php | 2 - .../CurrentDataTrackerProvider.php | 23 +---- src/Exception/IndexNotFoundException.php | 7 -- src/Logger/ActivityLogger.php | 4 +- .../PopulateCurrentDataTrackersHandler.php | 13 ++- .../UpdateActivityLogHandler.php | 4 +- .../Traits/NormalizationContextTrait.php} | 4 +- src/Storage/ActivityLogStorageInterface.php | 29 ++++++ .../CurrentDataTrackerStorageInterface.php | 25 ++++++ src/Storage/StorageInitializerInterface.php | 30 +++++++ tests/FunctionalTests/ActivityLogTest.php | 15 ++-- 26 files changed, 422 insertions(+), 221 deletions(-) create mode 100644 src/Bridge/Elasticsearch/Storage/ElasticsearchActivityLogStorage.php create mode 100644 src/Bridge/Elasticsearch/Storage/ElasticsearchCurrentDataTrackerStorage.php create mode 100644 src/Bridge/Elasticsearch/Storage/ElasticsearchStorageInitializer.php delete mode 100644 src/Exception/IndexNotFoundException.php rename src/{Bridge/Elasticsearch/Context/Traits/ElasticNormalizationContextTrait.php => Serializer/Traits/NormalizationContextTrait.php} (70%) create mode 100644 src/Storage/ActivityLogStorageInterface.php create mode 100644 src/Storage/CurrentDataTrackerStorageInterface.php create mode 100644 src/Storage/StorageInitializerInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb0b34..4309824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Storage abstraction: `ActivityLogStorageInterface`, `CurrentDataTrackerStorageInterface` and `StorageInitializerInterface` in the new `Locastic\Loggastic\Storage` namespace; implement and alias them to store activity logs in a custom backend +- Elasticsearch implementations of the storage interfaces in `Locastic\Loggastic\Bridge\Elasticsearch\Storage` (used by default, no configuration changes needed) - Support for Symfony 8 and DoctrineBundle 3 (`symfony/*` now `^6.4 || ^7.0 || ^8.0`, `doctrine/doctrine-bundle` now `^2.8 || ^3.0`), with Symfony 8 CI legs - Basic auth (`elastic_user`, `elastic_password`) and `elastic_ssl_verification` config options for secured Elasticsearch clusters (originally proposed in #26 by @jakubdusek) - Tests matrix now also runs against Elasticsearch 9 @@ -18,6 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Breaking:** migrated to the modern bundle directory layout: service configuration moved from `src/Resources/config/` to `config/`, and the bundle now extends `AbstractBundle`; the `LocasticLoggasticExtension` and `Configuration` classes were removed (see UPGRADE-2.0.md) - **Breaking:** upgraded to `elasticsearch/elasticsearch` `^8.0 || ^9.0`; Elasticsearch 7 servers are no longer supported and a PSR-18 HTTP client implementation is required (see UPGRADE-2.0.md) - **Breaking:** `ElasticsearchClientInterface::getClient()` now returns `Elastic\Elasticsearch\Client` instead of `Elasticsearch\Client` +- **Breaking:** core services (`ActivityLogProcessor`, `ActivityLogProvider`, `CurrentDataTrackerProvider`, `PopulateCurrentDataTrackersHandler` and the console commands) now depend on the storage interfaces instead of `ElasticsearchServiceInterface`/`ElasticsearchContextFactoryInterface`/`ElasticsearchIndexFactoryInterface`; their constructor signatures changed (see UPGRADE-2.0.md) +- **Breaking:** `ActivityLogProviderInterface::getActivityLogsByIndexAndId()` was removed; use `ElasticsearchActivityLogStorage::findByIndexAndObjectId()` for index-based reads +- **Breaking:** `ElasticNormalizationContextTrait` moved to `Locastic\Loggastic\Serializer\Traits\NormalizationContextTrait` (it is not Elasticsearch-specific) + +### Removed +- **Breaking:** `ActivityLogProvider::getCurrentDataTrackerByClassAndId()`, which declared an `?array` return type but returned an object and threw a `TypeError` on every hit; use `CurrentDataTrackerProviderInterface` instead +- **Breaking:** the unused `IndexNotFoundException` class ### Fixed - `ElasticsearchService::getItemById()` always returned null because it compared the hit total against an integer while Elasticsearch returns an object diff --git a/README.md b/README.md index fb93a63..b6cd6b0 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,9 @@ Here are the examples for displaying activity logs in twig or as Api endpoints: public function getActivityLogsByClass(string $className, array $sort = []): array; public function getActivityLogsByClassAndId(string $className, $objectId, array $sort = []): array; - - public function getActivityLogsByIndexAndId(string $index, $objectId, array $sort = []): array; ``` +If you need to read logs directly from a specific Elasticsearch index, use +`Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchActivityLogStorage::findByIndexAndObjectId()`. Use them to fetch data from the Elasticsearch and display it in your views. Example for displaying results in Twig: ```twig Activity logs for Blog Posts: @@ -199,6 +199,27 @@ Each time some change happens in the database for loggable entities, the activit ## Customization guide Now that you have the basic setup, you can add some additional options and customize the library to your needs. +### Custom storage backends +Activity logs are stored in Elasticsearch by default, but the core services only talk to three storage interfaces: + +```php +Locastic\Loggastic\Storage\ActivityLogStorageInterface # writes and reads activity logs +Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface # tracks the latest state of each loggable object +Locastic\Loggastic\Storage\StorageInitializerInterface # creates the underlying storage (indexes, tables, ...) +``` + +To store logs somewhere else, implement the three interfaces and alias them to your services: + +```yaml +# config/services.yaml +services: + Locastic\Loggastic\Storage\ActivityLogStorageInterface: '@App\Loggastic\MyActivityLogStorage' + Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface: '@App\Loggastic\MyCurrentDataTrackerStorage' + Locastic\Loggastic\Storage\StorageInitializerInterface: '@App\Loggastic\MyStorageInitializer' +``` + +The loggers, message handlers, data providers and console commands will use your implementations without any further changes. + ## Configuration reference Default configuration: ```yaml diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 5250e6a..27e1ce7 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -53,3 +53,35 @@ the deprecation shipped in a 1.x release so you can migrate before upgrading. (handled by `AttributeLoggableContextCollectionFactory`) or with XML/YAML extractors. The `doctrine/annotations` package is no longer a dependency since 1.2. + +## Storage abstraction + +Core services no longer talk to Elasticsearch directly. Three new interfaces +in `Locastic\Loggastic\Storage` sit between the core and the backend: +`ActivityLogStorageInterface`, `CurrentDataTrackerStorageInterface` and +`StorageInitializerInterface`. The default Elasticsearch implementations live +in `Locastic\Loggastic\Bridge\Elasticsearch\Storage` and are aliased to the +interfaces, so nothing changes unless you extended the affected classes: + +- The constructor signatures of `ActivityLogProcessor`, `ActivityLogProvider`, + `CurrentDataTrackerProvider`, `PopulateCurrentDataTrackersHandler`, + `CreateLoggableIndexesCommand` and `PopulateCurrentDataTrackersCommand` + changed: they now receive the storage interfaces instead of + `ElasticsearchServiceInterface`, `ElasticsearchContextFactoryInterface` and + `ElasticsearchIndexFactoryInterface`. Update any decorators or service + definitions overriding their arguments. +- `ActivityLogProviderInterface::getActivityLogsByIndexAndId()` was removed + from the interface because it leaks the Elasticsearch index name into the + storage-agnostic API. Inject + `Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchActivityLogStorage` + and call `findByIndexAndObjectId()` instead. +- `ActivityLogProvider::getCurrentDataTrackerByClassAndId()` was removed. It + declared an `?array` return type while the denormalizer returns an object, + so every call that found a tracker threw a `TypeError`. Use + `CurrentDataTrackerProviderInterface::getCurrentDataTrackerByClassAndId()`. +- `Locastic\Loggastic\Bridge\Elasticsearch\Context\Traits\ElasticNormalizationContextTrait` + moved to `Locastic\Loggastic\Serializer\Traits\NormalizationContextTrait`; + it only forces the `\DateTime::ATOM` date format and is not + Elasticsearch-specific. Update the import if you used it. +- The unused `Locastic\Loggastic\Exception\IndexNotFoundException` class was + removed. diff --git a/config/elastic.yaml b/config/elastic.yaml index 23a68cd..f44df43 100644 --- a/config/elastic.yaml +++ b/config/elastic.yaml @@ -42,4 +42,23 @@ services: Locastic\Loggastic\Bridge\Elasticsearch\Index\ElasticsearchIndexFactory: autowire: true +# Storage + Locastic\Loggastic\Storage\ActivityLogStorageInterface: + alias: 'Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchActivityLogStorage' + + Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchActivityLogStorage: + autowire: true + + Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface: + alias: 'Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchCurrentDataTrackerStorage' + + Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchCurrentDataTrackerStorage: + autowire: true + + Locastic\Loggastic\Storage\StorageInitializerInterface: + alias: 'Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchStorageInitializer' + + Locastic\Loggastic\Bridge\Elasticsearch\Storage\ElasticsearchStorageInitializer: + autowire: true + diff --git a/config/logger.yaml b/config/logger.yaml index 2d5bad6..35efe7e 100644 --- a/config/logger.yaml +++ b/config/logger.yaml @@ -24,9 +24,9 @@ services: Locastic\Loggastic\DataProcessor\ActivityLogProcessor: arguments: - - '@Locastic\Loggastic\Bridge\Elasticsearch\Context\ElasticsearchContextFactoryInterface' - '@serializer.normalizer.object' - - '@Locastic\Loggastic\Bridge\Elasticsearch\ElasticsearchServiceInterface' + - '@Locastic\Loggastic\Storage\ActivityLogStorageInterface' + - '@Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface' - '@Locastic\Loggastic\Factory\ActivityLogInputFactoryInterface' - '@Locastic\Loggastic\Factory\CurrentDataTrackerInputFactoryInterface' - '@Locastic\Loggastic\Metadata\LoggableContext\Factory\LoggableContextFactoryInterface' diff --git a/config/message_handlers.yaml b/config/message_handlers.yaml index cc1cc5a..e2dd53e 100644 --- a/config/message_handlers.yaml +++ b/config/message_handlers.yaml @@ -19,7 +19,6 @@ services: - '@Doctrine\Persistence\ManagerRegistry' - '@Locastic\Loggastic\Factory\CurrentDataTrackerInputFactoryInterface' - '@serializer.normalizer.object' - - '@Locastic\Loggastic\Bridge\Elasticsearch\ElasticsearchServiceInterface' - - '@Locastic\Loggastic\Bridge\Elasticsearch\Context\ElasticsearchContextFactoryInterface' + - '@Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface' tags: - { name: messenger.message_handler } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 484b02d..6284028 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -282,36 +282,6 @@ parameters: count: 1 path: src/DataProvider/ActivityLogProvider.php - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProvider\:\:getActivityLogsByIndexAndId\(\) has parameter \$objectId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/DataProvider/ActivityLogProvider.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProvider\:\:getActivityLogsByIndexAndId\(\) has parameter \$sort with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/DataProvider/ActivityLogProvider.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProvider\:\:getActivityLogsByIndexAndId\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/DataProvider/ActivityLogProvider.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProvider\:\:getCurrentDataTrackerByClassAndId\(\) has parameter \$objectId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/DataProvider/ActivityLogProvider.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProvider\:\:getCurrentDataTrackerByClassAndId\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/DataProvider/ActivityLogProvider.php - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProviderInterface\:\:getActivityLogsByClass\(\) has parameter \$sort with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -342,24 +312,6 @@ parameters: count: 1 path: src/DataProvider/ActivityLogProviderInterface.php - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProviderInterface\:\:getActivityLogsByIndexAndId\(\) has parameter \$objectId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: src/DataProvider/ActivityLogProviderInterface.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProviderInterface\:\:getActivityLogsByIndexAndId\(\) has parameter \$sort with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/DataProvider/ActivityLogProviderInterface.php - - - - message: '#^Method Locastic\\Loggastic\\DataProvider\\ActivityLogProviderInterface\:\:getActivityLogsByIndexAndId\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/DataProvider/ActivityLogProviderInterface.php - - message: '#^Method Locastic\\Loggastic\\DataProvider\\CurrentDataTrackerProvider\:\:getCurrentDataTrackerByClassAndId\(\) has parameter \$objectId with no type specified\.$#' identifier: missingType.parameter diff --git a/src/Bridge/Elasticsearch/ElasticsearchService.php b/src/Bridge/Elasticsearch/ElasticsearchService.php index e3988ef..50d3737 100644 --- a/src/Bridge/Elasticsearch/ElasticsearchService.php +++ b/src/Bridge/Elasticsearch/ElasticsearchService.php @@ -2,14 +2,14 @@ namespace Locastic\Loggastic\Bridge\Elasticsearch; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\Traits\ElasticNormalizationContextTrait; +use Locastic\Loggastic\Serializer\Traits\NormalizationContextTrait; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; final class ElasticsearchService implements ElasticsearchServiceInterface { - use ElasticNormalizationContextTrait; + use NormalizationContextTrait; public function __construct( private readonly ElasticsearchClientInterface $elasticsearchClient, diff --git a/src/Bridge/Elasticsearch/Storage/ElasticsearchActivityLogStorage.php b/src/Bridge/Elasticsearch/Storage/ElasticsearchActivityLogStorage.php new file mode 100644 index 0000000..e90ae27 --- /dev/null +++ b/src/Bridge/Elasticsearch/Storage/ElasticsearchActivityLogStorage.php @@ -0,0 +1,88 @@ +elasticsearchContextFactory->create($className); + + $this->elasticsearchService->createItem($activityLog, $elasticContext->getActivityLogIndex(), ['activity_log']); + } + + /** + * @param array $sort + * + * @return array + */ + public function findByClass(string $className, array $sort = [], int $limit = 20, int $offset = 0): array + { + $elasticContext = $this->elasticsearchContextFactory->create($className); + + $body = [ + 'sort' => $sort, + ]; + + // todo move class to config + return $this->elasticsearchService->getCollection( + $elasticContext->getActivityLogIndex(), + ActivityLog::class, + $body, + $limit, + $offset + ); + } + + /** + * @param array $sort + * + * @return array + */ + public function findByClassAndObjectId(string $className, mixed $objectId, array $sort = [], int $limit = 20, int $offset = 0): array + { + $elasticContext = $this->elasticsearchContextFactory->create($className); + + return $this->findByIndexAndObjectId($elasticContext->getActivityLogIndex(), $objectId, $sort, $limit, $offset); + } + + /** + * Elasticsearch-specific: reads activity logs directly from an index, bypassing + * the class name to index resolution. + * + * @param array $sort + * + * @return array + */ + public function findByIndexAndObjectId(string $index, mixed $objectId, array $sort = [], int $limit = 20, int $offset = 0): array + { + $body = [ + 'sort' => $sort, + 'query' => [ + 'term' => ['objectId' => $objectId], + ], + ]; + + // todo move class to config + return $this->elasticsearchService->getCollection( + $index, + ActivityLog::class, + $body, + $limit, + $offset + ); + } +} diff --git a/src/Bridge/Elasticsearch/Storage/ElasticsearchCurrentDataTrackerStorage.php b/src/Bridge/Elasticsearch/Storage/ElasticsearchCurrentDataTrackerStorage.php new file mode 100644 index 0000000..a7cb747 --- /dev/null +++ b/src/Bridge/Elasticsearch/Storage/ElasticsearchCurrentDataTrackerStorage.php @@ -0,0 +1,59 @@ +elasticsearchContextFactory->create($className); + + $this->elasticsearchService->createItem($currentDataTracker, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + } + + public function update(mixed $id, CurrentDataTrackerInputInterface $currentDataTracker, string $className): void + { + $elasticContext = $this->elasticsearchContextFactory->create($className); + + $this->elasticsearchService->updateItem($id, $currentDataTracker, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + } + + /** + * @param array $currentDataTrackers + */ + public function bulkSave(array $currentDataTrackers, string $className): void + { + $elasticContext = $this->elasticsearchContextFactory->create($className); + + $this->elasticsearchService->bulkCreate($currentDataTrackers, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + } + + public function findByClassAndObjectId(string $className, mixed $objectId): ?CurrentDataTrackerInterface + { + $elasticContext = $this->elasticsearchContextFactory->create($className); + + $body = [ + 'query' => ['term' => ['objectId' => $objectId]], + ]; + + // todo move class to config + return $this->elasticsearchService->getItemByQuery( + $elasticContext->getCurrentDataTrackerIndex(), + CurrentDataTracker::class, + $body + ); + } +} diff --git a/src/Bridge/Elasticsearch/Storage/ElasticsearchStorageInitializer.php b/src/Bridge/Elasticsearch/Storage/ElasticsearchStorageInitializer.php new file mode 100644 index 0000000..c11bd33 --- /dev/null +++ b/src/Bridge/Elasticsearch/Storage/ElasticsearchStorageInitializer.php @@ -0,0 +1,54 @@ +elasticsearchIndexFactory->createActivityLogIndex($className); + } catch (ClientResponseException $e) { + if (!str_contains($e->getMessage(), 'resource_already_exists_exception')) { + throw $e; + } + + return false; + } + + return true; + } + + public function initializeCurrentDataTrackerStorage(string $className): bool + { + try { + $this->elasticsearchIndexFactory->createCurrentDataTrackerLogIndex($className); + } catch (ClientResponseException $e) { + if (!str_contains($e->getMessage(), 'resource_already_exists_exception')) { + throw $e; + } + + return false; + } + + return true; + } + + public function recreateActivityLogStorage(string $className): void + { + $this->elasticsearchIndexFactory->recreateActivityLogIndex($className); + } + + public function recreateCurrentDataTrackerStorage(string $className): void + { + $this->elasticsearchIndexFactory->recreateCurrentDataTrackerLogIndex($className); + } +} diff --git a/src/Command/CreateLoggableIndexesCommand.php b/src/Command/CreateLoggableIndexesCommand.php index 616deaa..e4dc7e3 100644 --- a/src/Command/CreateLoggableIndexesCommand.php +++ b/src/Command/CreateLoggableIndexesCommand.php @@ -2,9 +2,8 @@ namespace Locastic\Loggastic\Command; -use Elastic\Elasticsearch\Exception\ClientResponseException; -use Locastic\Loggastic\Bridge\Elasticsearch\Index\ElasticsearchIndexFactoryInterface; use Locastic\Loggastic\Metadata\LoggableContext\Factory\LoggableContextCollectionFactoryInterface; +use Locastic\Loggastic\Storage\StorageInitializerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -14,7 +13,7 @@ #[AsCommand('locastic:activity-logs:create-loggable-indexes')] final class CreateLoggableIndexesCommand extends Command { - public function __construct(private readonly LoggableContextCollectionFactoryInterface $loggableContextCollectionFactory, private readonly ElasticsearchIndexFactoryInterface $elasticsearchIndexFactory) + public function __construct(private readonly LoggableContextCollectionFactoryInterface $loggableContextCollectionFactory, private readonly StorageInitializerInterface $storageInitializer) { parent::__construct(); } @@ -29,26 +28,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($loggableContextCollection->getIterator() as $loggableClass => $config) { $io->writeln('Creating '.$loggableClass.' activity_log index'); - try { - $this->elasticsearchIndexFactory->createActivityLogIndex($loggableClass); - } catch (ClientResponseException $e) { - if (str_contains($e->getMessage(), 'resource_already_exists_exception')) { - $output->writeln('Index already exists, skipping.'); - } else { - throw $e; - } + if (!$this->storageInitializer->initializeActivityLogStorage($loggableClass)) { + $output->writeln('Index already exists, skipping.'); } $io->writeln('Creating '.$loggableClass.' current_data_tracker index'); - try { - $this->elasticsearchIndexFactory->createCurrentDataTrackerLogIndex($loggableClass); - } catch (ClientResponseException $e) { - if (str_contains($e->getMessage(), 'resource_already_exists_exception')) { - $output->writeln('Index already exists, skipping.'); - } else { - throw $e; - } + if (!$this->storageInitializer->initializeCurrentDataTrackerStorage($loggableClass)) { + $output->writeln('Index already exists, skipping.'); } } diff --git a/src/Command/PopulateCurrentDataTrackersCommand.php b/src/Command/PopulateCurrentDataTrackersCommand.php index dd6b325..ae857fb 100644 --- a/src/Command/PopulateCurrentDataTrackersCommand.php +++ b/src/Command/PopulateCurrentDataTrackersCommand.php @@ -3,9 +3,9 @@ namespace Locastic\Loggastic\Command; use Doctrine\Persistence\ManagerRegistry; -use Locastic\Loggastic\Bridge\Elasticsearch\Index\ElasticsearchIndexFactoryInterface; use Locastic\Loggastic\Message\PopulateCurrentDataTrackersMessage; use Locastic\Loggastic\Metadata\LoggableContext\Factory\LoggableContextCollectionFactoryInterface; +use Locastic\Loggastic\Storage\StorageInitializerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -22,7 +22,7 @@ #[AsCommand('locastic:activity-logs:populate-current-data-trackers')] final class PopulateCurrentDataTrackersCommand extends Command { - public function __construct(private readonly ElasticsearchIndexFactoryInterface $elasticsearchIndexFactory, private readonly LoggableContextCollectionFactoryInterface $loggableContextCollectionFactory, private readonly ManagerRegistry $managerRegistry, private readonly MessageBusInterface $bus) + public function __construct(private readonly StorageInitializerInterface $storageInitializer, private readonly LoggableContextCollectionFactoryInterface $loggableContextCollectionFactory, private readonly ManagerRegistry $managerRegistry, private readonly MessageBusInterface $bus) { parent::__construct(); } @@ -68,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $this->elasticsearchIndexFactory->recreateCurrentDataTrackerLogIndex($loggableClass); + $this->storageInitializer->recreateCurrentDataTrackerStorage($loggableClass); } catch (\Exception $e) { $io->error($e->getMessage()); diff --git a/src/DataProcessor/ActivityLogProcessor.php b/src/DataProcessor/ActivityLogProcessor.php index 8847037..b7c5d3a 100644 --- a/src/DataProcessor/ActivityLogProcessor.php +++ b/src/DataProcessor/ActivityLogProcessor.php @@ -2,9 +2,6 @@ namespace Locastic\Loggastic\DataProcessor; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\ElasticsearchContextFactoryInterface; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\Traits\ElasticNormalizationContextTrait; -use Locastic\Loggastic\Bridge\Elasticsearch\ElasticsearchServiceInterface; use Locastic\Loggastic\Factory\ActivityLogInputFactoryInterface; use Locastic\Loggastic\Factory\CurrentDataTrackerInputFactoryInterface; use Locastic\Loggastic\Message\CreateActivityLogMessageInterface; @@ -12,17 +9,20 @@ use Locastic\Loggastic\Message\UpdateActivityLogMessageInterface; use Locastic\Loggastic\Metadata\LoggableContext\Factory\LoggableContextFactoryInterface; use Locastic\Loggastic\Model\Output\CurrentDataTrackerInterface; +use Locastic\Loggastic\Serializer\Traits\NormalizationContextTrait; +use Locastic\Loggastic\Storage\ActivityLogStorageInterface; +use Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface; use Locastic\Loggastic\Util\ArraysComparer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; final class ActivityLogProcessor implements ActivityLogProcessorInterface { - use ElasticNormalizationContextTrait; + use NormalizationContextTrait; public function __construct( - private readonly ElasticsearchContextFactoryInterface $elasticsearchContextFactory, private readonly NormalizerInterface $objectNormalizer, - private readonly ElasticsearchServiceInterface $elasticService, + private readonly ActivityLogStorageInterface $activityLogStorage, + private readonly CurrentDataTrackerStorageInterface $currentDataTrackerStorage, private readonly ActivityLogInputFactoryInterface $activityLogInputFactory, private readonly CurrentDataTrackerInputFactoryInterface $currentDataTrackerInputFactory, private readonly LoggableContextFactoryInterface $loggableContextFactory, @@ -39,15 +39,13 @@ public function processCreatedItem(CreateActivityLogMessageInterface $message): $normalizedItem = $this->objectNormalizer->normalize($message->getItem(), 'activityLog', $this->getNormalizationContext($loggableContext)); - $elasticContext = $this->elasticsearchContextFactory->create($message->getClassName()); - // create log to save full item data for later comparison $currentDataTracker = $this->currentDataTrackerInputFactory->create($message->getItem(), $normalizedItem); - $this->elasticService->createItem($currentDataTracker, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + $this->currentDataTrackerStorage->save($currentDataTracker, $message->getClassName()); // create log for item creation $activityLog = $this->activityLogInputFactory->createFromActivityLogMessage($message); - $this->elasticService->createItem($activityLog, $elasticContext->getActivityLogIndex(), ['activity_log']); + $this->activityLogStorage->save($activityLog, $message->getClassName()); } public function processUpdatedItem(UpdateActivityLogMessageInterface $message, CurrentDataTrackerInterface $currentDataTracker): void @@ -73,18 +71,16 @@ public function processUpdatedItem(UpdateActivityLogMessageInterface $message, C return; } - $elasticContext = $this->elasticsearchContextFactory->create($message->getClassName()); - // create log $activityLog = $this->activityLogInputFactory->createFromActivityLogMessage($message, $changes); - $this->elasticService->createItem($activityLog, $elasticContext->getActivityLogIndex(), ['activity_log']); + $this->activityLogStorage->save($activityLog, $message->getClassName()); // update full data log $currentDataTrackerInput = $this->currentDataTrackerInputFactory->createFromCurrentDataTracker($currentDataTracker); $currentDataTrackerInput->setData(json_encode($updatedData, JSON_THROW_ON_ERROR)); - $this->elasticService->updateItem($currentDataTracker->getId(), $currentDataTrackerInput, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + $this->currentDataTrackerStorage->update($currentDataTracker->getId(), $currentDataTrackerInput, $message->getClassName()); } public function processDeletedItem(DeleteActivityLogMessageInterface $message): void @@ -97,7 +93,6 @@ public function processDeletedItem(DeleteActivityLogMessageInterface $message): $activityLog = $this->activityLogInputFactory->createFromActivityLogMessage($message); - $elasticContext = $this->elasticsearchContextFactory->create($message->getClassName()); - $this->elasticService->createItem($activityLog, $elasticContext->getActivityLogIndex(), ['activity_log']); + $this->activityLogStorage->save($activityLog, $message->getClassName()); } } diff --git a/src/DataProvider/ActivityLogProvider.php b/src/DataProvider/ActivityLogProvider.php index f3c7106..eaa9b92 100644 --- a/src/DataProvider/ActivityLogProvider.php +++ b/src/DataProvider/ActivityLogProvider.php @@ -2,51 +2,17 @@ namespace Locastic\Loggastic\DataProvider; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\ElasticsearchContextFactoryInterface; -use Locastic\Loggastic\Bridge\Elasticsearch\ElasticsearchServiceInterface; -use Locastic\Loggastic\Model\Output\ActivityLog; -use Locastic\Loggastic\Model\Output\CurrentDataTracker; +use Locastic\Loggastic\Storage\ActivityLogStorageInterface; final class ActivityLogProvider implements ActivityLogProviderInterface { - public function __construct( - private readonly ElasticsearchServiceInterface $elasticsearchService, - private readonly ElasticsearchContextFactoryInterface $elasticsearchContextFactory, - ) { - } - - public function getActivityLogsByClass(string $className, array $sort = [], int $limit = 20, int $offset = 0): array + public function __construct(private readonly ActivityLogStorageInterface $activityLogStorage) { - $elasticContext = $this->elasticsearchContextFactory->create($className); - - $body = [ - 'sort' => $sort, - ]; - - // todo move class to config - return $this->elasticsearchService->getCollection( - $elasticContext->getActivityLogIndex(), - ActivityLog::class, - $body, - $limit, - $offset - ); } - public function getCurrentDataTrackerByClassAndId(string $className, $objectId): ?array + public function getActivityLogsByClass(string $className, array $sort = [], int $limit = 20, int $offset = 0): array { - $elasticContext = $this->elasticsearchContextFactory->create($className); - - $body = [ - 'query' => ['term' => ['objectId' => $objectId]], - ]; - - // todo move class to config - return $this->elasticsearchService->getItemByQuery( - $elasticContext->getCurrentDataTrackerIndex(), - CurrentDataTracker::class, - $body - ); + return $this->activityLogStorage->findByClass($className, $sort, $limit, $offset); } public function getActivityLogsByClassAndId( @@ -56,46 +22,6 @@ public function getActivityLogsByClassAndId( int $limit = 20, int $offset = 0, ): array { - $elasticContext = $this->elasticsearchContextFactory->create($className); - - $body = [ - 'sort' => $sort, - 'query' => [ - 'term' => ['objectId' => $objectId], - ], - ]; - - // todo move class to config - return $this->elasticsearchService->getCollection( - $elasticContext->getActivityLogIndex(), - ActivityLog::class, - $body, - $limit, - $offset - ); - } - - public function getActivityLogsByIndexAndId( - string $index, - $objectId, - array $sort = [], - int $limit = 20, - int $offset = 0, - ): array { - $body = [ - 'sort' => $sort, - 'query' => [ - 'term' => ['objectId' => $objectId], - ], - ]; - - // todo move class to config - return $this->elasticsearchService->getCollection( - $index, - ActivityLog::class, - $body, - $limit, - $offset - ); + return $this->activityLogStorage->findByClassAndObjectId($className, $objectId, $sort, $limit, $offset); } } diff --git a/src/DataProvider/ActivityLogProviderInterface.php b/src/DataProvider/ActivityLogProviderInterface.php index c7159cf..5421857 100644 --- a/src/DataProvider/ActivityLogProviderInterface.php +++ b/src/DataProvider/ActivityLogProviderInterface.php @@ -7,6 +7,4 @@ interface ActivityLogProviderInterface public function getActivityLogsByClass(string $className, array $sort = [], int $limit = 20, int $offset = 0): array; public function getActivityLogsByClassAndId(string $className, $objectId, array $sort = [], int $limit = 20, int $offset = 0): array; - - public function getActivityLogsByIndexAndId(string $index, $objectId, array $sort = [], int $limit = 20, int $offset = 0): array; } diff --git a/src/DataProvider/CurrentDataTrackerProvider.php b/src/DataProvider/CurrentDataTrackerProvider.php index 7108107..e86dad1 100644 --- a/src/DataProvider/CurrentDataTrackerProvider.php +++ b/src/DataProvider/CurrentDataTrackerProvider.php @@ -2,32 +2,17 @@ namespace Locastic\Loggastic\DataProvider; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\ElasticsearchContextFactoryInterface; -use Locastic\Loggastic\Bridge\Elasticsearch\ElasticsearchServiceInterface; -use Locastic\Loggastic\Model\Output\CurrentDataTracker; use Locastic\Loggastic\Model\Output\CurrentDataTrackerInterface; +use Locastic\Loggastic\Storage\CurrentDataTrackerStorageInterface; final class CurrentDataTrackerProvider implements CurrentDataTrackerProviderInterface { - public function __construct( - private readonly ElasticsearchServiceInterface $elasticsearchService, - private readonly ElasticsearchContextFactoryInterface $elasticsearchContextFactory, - ) { + public function __construct(private readonly CurrentDataTrackerStorageInterface $currentDataTrackerStorage) + { } public function getCurrentDataTrackerByClassAndId(string $className, $objectId): ?CurrentDataTrackerInterface { - $elasticContext = $this->elasticsearchContextFactory->create($className); - - $body = [ - 'query' => ['term' => ['objectId' => $objectId]], - ]; - - // todo move class to config - return $this->elasticsearchService->getItemByQuery( - $elasticContext->getCurrentDataTrackerIndex(), - CurrentDataTracker::class, - $body - ); + return $this->currentDataTrackerStorage->findByClassAndObjectId($className, $objectId); } } diff --git a/src/Exception/IndexNotFoundException.php b/src/Exception/IndexNotFoundException.php deleted file mode 100644 index 4532852..0000000 --- a/src/Exception/IndexNotFoundException.php +++ /dev/null @@ -1,7 +0,0 @@ -currentDataTrackerInputFactory->create($item, $normalizedItem); } - $elasticContext = $this->elasticsearchContextFactory->create($message->getLoggableClass()); - $this->elasticService->bulkCreate($currentDataTrackers, $elasticContext->getCurrentDataTrackerIndex(), ['current_data_tracker']); + $this->currentDataTrackerStorage->bulkSave($currentDataTrackers, $message->getLoggableClass()); } } diff --git a/src/MessageHandler/UpdateActivityLogHandler.php b/src/MessageHandler/UpdateActivityLogHandler.php index 20795f7..f1bb030 100644 --- a/src/MessageHandler/UpdateActivityLogHandler.php +++ b/src/MessageHandler/UpdateActivityLogHandler.php @@ -2,18 +2,18 @@ namespace Locastic\Loggastic\MessageHandler; -use Locastic\Loggastic\Bridge\Elasticsearch\Context\Traits\ElasticNormalizationContextTrait; use Locastic\Loggastic\DataProcessor\ActivityLogProcessorInterface; use Locastic\Loggastic\DataProvider\CurrentDataTrackerProviderInterface; use Locastic\Loggastic\Message\UpdateActivityLogMessageInterface; use Locastic\Loggastic\Metadata\LoggableContext\Factory\LoggableContextFactory; use Locastic\Loggastic\Model\Output\CurrentDataTrackerInterface; +use Locastic\Loggastic\Serializer\Traits\NormalizationContextTrait; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] final class UpdateActivityLogHandler { - use ElasticNormalizationContextTrait; + use NormalizationContextTrait; public function __construct( private readonly ActivityLogProcessorInterface $activityLogProcessor, diff --git a/src/Bridge/Elasticsearch/Context/Traits/ElasticNormalizationContextTrait.php b/src/Serializer/Traits/NormalizationContextTrait.php similarity index 70% rename from src/Bridge/Elasticsearch/Context/Traits/ElasticNormalizationContextTrait.php rename to src/Serializer/Traits/NormalizationContextTrait.php index f6510dc..3da2a6d 100644 --- a/src/Bridge/Elasticsearch/Context/Traits/ElasticNormalizationContextTrait.php +++ b/src/Serializer/Traits/NormalizationContextTrait.php @@ -1,10 +1,10 @@ $sort map of field name to direction ('asc' or 'desc') + * + * @return array + */ + public function findByClass(string $className, array $sort = [], int $limit = 20, int $offset = 0): array; + + /** + * @param array $sort map of field name to direction ('asc' or 'desc') + * + * @return array + */ + public function findByClassAndObjectId(string $className, mixed $objectId, array $sort = [], int $limit = 20, int $offset = 0): array; +} diff --git a/src/Storage/CurrentDataTrackerStorageInterface.php b/src/Storage/CurrentDataTrackerStorageInterface.php new file mode 100644 index 0000000..6b2bc71 --- /dev/null +++ b/src/Storage/CurrentDataTrackerStorageInterface.php @@ -0,0 +1,25 @@ + $currentDataTrackers + */ + public function bulkSave(array $currentDataTrackers, string $className): void; + + public function findByClassAndObjectId(string $className, mixed $objectId): ?CurrentDataTrackerInterface; +} diff --git a/src/Storage/StorageInitializerInterface.php b/src/Storage/StorageInitializerInterface.php new file mode 100644 index 0000000..ad225d2 --- /dev/null +++ b/src/Storage/StorageInitializerInterface.php @@ -0,0 +1,30 @@ +get('test.service_container'); $this->activityLogProvider = $container->get(ActivityLogProviderInterface::class); + $this->elasticsearchActivityLogStorage = $container->get(ElasticsearchActivityLogStorage::class); $this->activityLogger = $container->get(ActivityLoggerInterface::class); // prepare elastic index - $elasticsearchIndexFactory = $container->get(ElasticsearchIndexFactoryInterface::class); - $elasticsearchIndexFactory->recreateActivityLogIndex(DummyBlogPost::class); - $elasticsearchIndexFactory->recreateCurrentDataTrackerLogIndex(DummyBlogPost::class); + $storageInitializer = $container->get(StorageInitializerInterface::class); + $storageInitializer->recreateActivityLogStorage(DummyBlogPost::class); + $storageInitializer->recreateCurrentDataTrackerStorage(DummyBlogPost::class); $this->blogPost = $this->createDummyBlogPost(); } @@ -161,7 +164,7 @@ public function testLogProviderByClassAndIdLimit(): void public function testProviderByClassAndIndexLimit(): void { - $activityLogs = $this->activityLogProvider->getActivityLogsByIndexAndId('dummy_blog_post_activity_log', 15, [], 1); + $activityLogs = $this->elasticsearchActivityLogStorage->findByIndexAndObjectId('dummy_blog_post_activity_log', 15, [], 1); self::assertCount(1, $activityLogs); } @@ -192,7 +195,7 @@ public function testLogProviderByClassAndIdLimitAndOffset(): void public function testProviderByClassAndIndexLimitAndOffset(): void { - $activityLogs = $this->activityLogProvider->getActivityLogsByIndexAndId('dummy_blog_post_activity_log', 15, [], 20, 3); + $activityLogs = $this->elasticsearchActivityLogStorage->findByIndexAndObjectId('dummy_blog_post_activity_log', 15, [], 20, 3); $editedLog = $activityLogs[0];