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];