From 8a7104f8b77631bb592c9d76540adc14bbc6ac9f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:11:17 +0100 Subject: [PATCH] WIP: Reintroduced initial `EventMigrationService` During extending phpstan to more packages (https://github.com/neos/neos-development-collection/pull/4650) the migration was dropped. The code had become already outdated by then as the api was changed a lot. This service should function as boilerplate for new migrations. --- ...NotAllowedOutsideContentRepositoryRule.php | 2 + .../MigrateEventsCommandController.php | 37 +++++ .../Classes/Service/EventMigrationService.php | 143 ++++++++++++++++++ .../Service/EventMigrationServiceFactory.php | 41 +++++ 4 files changed, 223 insertions(+) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php create mode 100644 Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationServiceFactory.php diff --git a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php index 20ce94d1592..8d34a40200d 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/PhpstanRules/InternalMethodsNotAllowedOutsideContentRepositoryRule.php @@ -40,6 +40,8 @@ public function processNode(Node $node, Scope $scope): array || str_starts_with($scope->getNamespace(), 'Neos\ContentRepository\Export') || str_starts_with($scope->getNamespace(), 'Neos\ContentRepository\LegacyNodeMigration') || str_starts_with($scope->getNamespace(), 'Neos\ContentRepository\StructureAdjustment') + // we do some next level stuff in the migrations, and thus we do illegal stuff: + || str_ends_with($scope->getFile(), 'Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php') ) ) { // todo this rule was intended to enforce the internal annotations from the Neos\ContentRepository\Core from all call sites. diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php new file mode 100644 index 00000000000..9fab788c7df --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Command/MigrateEventsCommandController.php @@ -0,0 +1,37 @@ +contentRepositoryRegistry->buildService($contentRepositoryId, $this->eventMigrationServiceFactory); + $eventMigrationService->fillAffectedDimensionSpacePointsInNodePropertiesWereSet($this->outputLine(...)); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php new file mode 100644 index 00000000000..58560e522e3 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationService.php @@ -0,0 +1,143 @@ +contentRepositoryId) + . '_bak_' . date('Y_m_d_H_i_s'); + $outputFn('Backup: copying events table to %s', [$backupEventTableName]); + $this->copyEventTable($backupEventTableName); + + $outputFn('Backup completed. Resetting Graph Projection.'); + $this->contentRepository->resetProjectionState(ContentGraphProjection::class); + + $contentGraphProjection = $this->projections->get(ContentGraphProjection::class); + $contentGraph = $contentGraphProjection->getState(); + assert($contentGraph instanceof ContentGraphInterface); + + $streamName = VirtualStreamName::all(); + $eventStream = $this->eventStore->load($streamName); + foreach ($eventStream as $eventEnvelope) { + if ($eventEnvelope->event->type->value === 'NodePropertiesWereSet') { + $eventData = json_decode($eventEnvelope->event->data->value, true); + if (!isset($eventData['affectedDimensionSpacePoints'])) { + // Replay the projection until before the current NodePropertiesWereSet event + $contentGraphProjection->catchUp( + $eventStream->withMaximumSequenceNumber($eventEnvelope->sequenceNumber->previous()), + $this->contentRepository + ); + + // now we can ask the NodeAggregate (read model) for the covered DSPs. + $nodeAggregate = $contentGraph->findNodeAggregateById( + ContentStreamId::fromString($eventData['contentStreamId']), + NodeAggregateId::fromString($eventData['nodeAggregateId']) + ); + $affectedDimensionSpacePoints = $nodeAggregate->getCoverageByOccupant( + OriginDimensionSpacePoint::fromArray($eventData['originDimensionSpacePoint']) + ); + + // ... and update the event + $eventData['affectedDimensionSpacePoints'] = $affectedDimensionSpacePoints->jsonSerialize(); + $outputFn( + 'Rewriting %s: (%s, Origin: %s) => Affected: %s', + [ + $eventEnvelope->sequenceNumber->value, + $eventEnvelope->event->type->value, + json_encode($eventData['originDimensionSpacePoint']), + json_encode($eventData['affectedDimensionSpacePoints']) + ] + ); + $this->updateEvent($eventEnvelope->sequenceNumber, $eventData); + } + } + } + + $outputFn('Rewriting completed. Now catching up the GraphProjection to final state.'); + $contentGraphProjection->catchUp($eventStream, $this->contentRepository); + + if ($this->projections->has(DocumentUriPathProjection::class)) { + $outputFn('Found DocumentUriPathProjection. Will replay this, as it relies on the updated affectedDimensionSpacePoints'); + $documentUriPathProjection = $this->projections->get(DocumentUriPathProjection::class); + $documentUriPathProjection->reset(); + $documentUriPathProjection->catchUp($eventStream, $this->contentRepository); + } + + $outputFn('All done.'); + } + + + private function updateEvent(SequenceNumber $sequenceNumber, array $eventData) + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId); + $this->connection->beginTransaction(); + $this->connection->executeStatement( + 'UPDATE ' . $eventTableName . ' SET payload=:payload WHERE sequencenumber=:sequenceNumber', + [ + 'payload' => json_encode($eventData), + 'sequenceNumber' => $sequenceNumber->value + ] + ); + $this->connection->commit(); + } + + private function copyEventTable(string $backupEventTableName) + { + $eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId); + $this->connection->executeStatement( + 'CREATE TABLE ' . $backupEventTableName . ' AS + SELECT * + FROM ' . $eventTableName + ); + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationServiceFactory.php new file mode 100644 index 00000000000..186ec328347 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Service/EventMigrationServiceFactory.php @@ -0,0 +1,41 @@ + + * @internal this is currently only used by the {@see MigrateEventsCommandController} + */ +#[Flow\Scope("singleton")] +final class EventMigrationServiceFactory implements ContentRepositoryServiceFactoryInterface +{ + public function __construct( + private readonly Connection $connection, + ) {} + + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface + { + if (!($serviceFactoryDependencies->eventStore instanceof DoctrineEventStore)) { + throw new \RuntimeException('EventMigrationService only works with DoctrineEventStore, ' . get_class($serviceFactoryDependencies->eventStore) . ' given'); + } + + return new EventMigrationService( + $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->contentRepositoryId, + $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, + $this->connection + ); + } +}