diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php
index 020e0f23b80..38528096279 100644
--- a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php
+++ b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php
@@ -157,6 +157,7 @@ public function denormalize(Event $event): EventInterface
);
}
assert(is_array($eventDataAsArray));
+ /** {@see EventInterface::fromArray()} */
return $eventClassName::fromArray($eventDataAsArray);
}
}
diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php
index 10b5a0f2d03..c7de2e5bb28 100644
--- a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php
+++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php
@@ -15,7 +15,9 @@
namespace Neos\ContentRepository\Core\SharedModel\ContentRepository;
use Neos\ContentRepository\Core\Projection\ProjectionStatuses;
+use Neos\ContentRepository\Core\Projection\ProjectionStatusType;
use Neos\EventStore\Model\EventStore\Status as EventStoreStatus;
+use Neos\EventStore\Model\EventStore\StatusType as EventStoreStatusType;
/**
* @api
@@ -27,4 +29,17 @@ public function __construct(
public ProjectionStatuses $projectionStatuses,
) {
}
+
+ public function isOk(): bool
+ {
+ if ($this->eventStoreStatus->type !== EventStoreStatusType::OK) {
+ return false;
+ }
+ foreach ($this->projectionStatuses as $projectionStatus) {
+ if ($projectionStatus->type !== ProjectionStatusType::OK) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php
index 6f6fe55ff91..52ee49ebd52 100644
--- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php
+++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php
@@ -122,10 +122,9 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config =
$this->quit();
}
$this->connection->executeStatement('TRUNCATE ' . $connection->quoteIdentifier($eventTableName));
- // we also need to reset the projections; in order to ensure the system runs deterministically. We
- // do this by replaying the just-truncated event stream.
+ // we also need to reset the projections; in order to ensure the system runs deterministically
$projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory);
- $projectionService->replayAllProjections(CatchUpOptions::create());
+ $projectionService->resetAllProjections();
$this->outputLine('Truncated events');
$liveContentStreamId = ContentStreamId::create();
diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php
index e4672f9885d..c82eebe9106 100644
--- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php
+++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php
@@ -5,7 +5,6 @@
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
use Neos\ContentRepository\Core\Projection\CatchUpOptions;
-use Neos\ContentRepository\Core\Projection\ProjectionStatus;
use Neos\ContentRepository\Core\Projection\ProjectionStatusType;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory;
@@ -33,11 +32,27 @@ public function __construct(
* Note: This command is non-destructive, i.e. it can be executed without side effects even if all dependencies are up-to-date
* Therefore it makes sense to include this command into the Continuous Integration
*
+ * To check if the content repository needs to be setup look into cr:status.
+ * That command will also display information what is about to be migrated.
+ *
* @param string $contentRepository Identifier of the Content Repository to set up
+ * @param bool $resetProjections Advanced. Can be used in rare cases when the projections cannot be migrated to reset everything in advance. This requires a full replay afterwards.
*/
- public function setupCommand(string $contentRepository = 'default'): void
+ public function setupCommand(string $contentRepository = 'default', bool $resetProjections = false): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
+
+ if ($resetProjections) {
+ if (!$this->output->askConfirmation(sprintf('> Advanced Mode. The flag --reset-projections will reset all projections in "%s", which leaves you with empty projections to be replayed. Are you sure to proceed? (y/n) ', $contentRepositoryId->value), false)) {
+ $this->outputLine('Abort.');
+ return;
+ }
+
+ $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory);
+ $projectionService->resetAllProjections();
+ $this->outputLine('All projections of Content Repository "%s" were resettet.', [$contentRepositoryId->value]);
+ }
+
$this->contentRepositoryRegistry->get($contentRepositoryId)->setUp();
$this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]);
}
@@ -45,6 +60,8 @@ public function setupCommand(string $contentRepository = 'default'): void
/**
* Determine and output the status of the event store and all registered projections for a given Content Repository
*
+ * In verbose mode it will also display information what should and will be migrated when cr:setup is used.
+ *
* @param string $contentRepository Identifier of the Content Repository to determine the status for
* @param bool $verbose If set, more details will be shown
* @param bool $quiet If set, no output is generated. This is useful if only the exit code (0 = all OK, 1 = errors or warnings) is of interest
@@ -57,8 +74,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$status = $this->contentRepositoryRegistry->get($contentRepositoryId)->status();
- $hasErrorsOrWarnings = false;
-
$this->output('Event Store: ');
$this->outputLine(match ($status->eventStoreStatus->type) {
StatusType::OK => 'OK',
@@ -68,9 +83,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo
if ($verbose && $status->eventStoreStatus->details !== '') {
$this->outputFormatted($status->eventStoreStatus->details, [], 2);
}
- if ($status->eventStoreStatus->type !== StatusType::OK) {
- $hasErrorsOrWarnings = true;
- }
$this->outputLine();
foreach ($status->projectionStatuses as $projectionName => $projectionStatus) {
$this->output('Projection "%s": ', [$projectionName]);
@@ -80,9 +92,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo
ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!',
ProjectionStatusType::ERROR => 'ERROR',
});
- if ($projectionStatus->type !== ProjectionStatusType::OK) {
- $hasErrorsOrWarnings = true;
- }
if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) {
$lines = explode(chr(10), $projectionStatus->details ?: 'No details available.');
foreach ($lines as $line) {
@@ -91,21 +100,32 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo
$this->outputLine();
}
}
- if ($hasErrorsOrWarnings) {
+ if (!$status->isOk()) {
$this->quit(1);
}
}
/**
- * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup
+ * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup.
*
* @param string $projection Full Qualified Class Name or alias of the projection to replay (e.g. "contentStream")
* @param string $contentRepository Identifier of the Content Repository instance to operate on
- * @param bool $quiet If set only fatal errors are rendered to the output
+ * @param bool $force Replay the projection without confirmation. This may take some time!
+ * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input)
* @param int $until Until which sequence number should projections be replayed? useful for debugging
*/
- public function replayCommand(string $projection, string $contentRepository = 'default', bool $quiet = false, int $until = 0): void
+ public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void
{
+ if (!$force && $quiet) {
+ $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.');
+ $this->quit(1);
+ }
+
+ if (!$force && !$this->output->askConfirmation(sprintf('> This will replay the projection "%s" in "%s", which may take some time. Are you sure to proceed? (y/n) ', $projection, $contentRepository), false)) {
+ $this->outputLine('Abort.');
+ return;
+ }
+
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory);
@@ -131,19 +151,29 @@ public function replayCommand(string $projection, string $contentRepository = 'd
* Replays all projections of the specified Content Repository by resetting their states and performing a full catchup
*
* @param string $contentRepository Identifier of the Content Repository instance to operate on
- * @param bool $quiet If set only fatal errors are rendered to the output
+ * @param bool $force Replay the projection without confirmation. This may take some time!
+ * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input)
*/
- public function replayAllCommand(string $contentRepository = 'default', bool $quiet = false): void
+ public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void
{
+ if (!$force && $quiet) {
+ $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.');
+ $this->quit(1);
+ }
+
+ if (!$force && !$this->output->askConfirmation(sprintf('> This will replay all projections in "%s", which may take some time. Are you sure to proceed? (y/n) ', $contentRepository), false)) {
+ $this->outputLine('Abort.');
+ return;
+ }
+
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory);
if (!$quiet) {
$this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]);
- // TODO start progress bar
}
- $projectionService->replayAllProjections(CatchUpOptions::create());
+ // TODO progress bar with all events? Like projectionReplayCommand?
+ $projectionService->replayAllProjections(CatchUpOptions::create(), fn (string $projectionAlias) => $this->outputLine(sprintf(' * replaying %s projection', $projectionAlias)));
if (!$quiet) {
- // TODO finish progress bar
$this->outputLine('Done.');
}
}
@@ -151,12 +181,14 @@ public function replayAllCommand(string $contentRepository = 'default', bool $qu
/**
* This will completely prune the data of the specified content repository.
*
- * @param string $contentRepository name of the content repository where the data should be pruned from.
+ * @param string $contentRepository Name of the content repository where the data should be pruned from.
+ * @param bool $force Prune the cr without confirmation. This cannot be reverted!
* @return void
*/
- public function pruneCommand(string $contentRepository = 'default'): void
+ public function pruneCommand(string $contentRepository = 'default', bool $force = false): void
{
- if (!$this->output->askConfirmation(sprintf("This will prune your content repository \"%s\". Are you sure to proceed? (y/n) ", $contentRepository), false)) {
+ if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) {
+ $this->outputLine('Abort.');
return;
}
@@ -166,14 +198,23 @@ public function pruneCommand(string $contentRepository = 'default'): void
$contentRepositoryId,
new ContentStreamPrunerFactory()
);
- $contentStreamPruner->pruneAll();
$workspaceMaintenanceService = $this->contentRepositoryRegistry->buildService(
$contentRepositoryId,
new WorkspaceMaintenanceServiceFactory()
);
+
+ $projectionService = $this->contentRepositoryRegistry->buildService(
+ $contentRepositoryId,
+ $this->projectionServiceFactory
+ );
+
+ // reset the events table
+ $contentStreamPruner->pruneAll();
$workspaceMaintenanceService->pruneAll();
+ // reset the projections state
+ $projectionService->resetAllProjections();
- $this->replayAllCommand($contentRepository);
+ $this->outputLine('Done.');
}
}
diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php
index 9dec35da709..2dcf16f96cc 100644
--- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php
+++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php
@@ -31,14 +31,24 @@ public function replayProjection(string $projectionAliasOrClassName, CatchUpOpti
$this->contentRepository->catchUpProjection($projectionClassName, $options);
}
- public function replayAllProjections(CatchUpOptions $options): void
+ public function replayAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void
{
foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) {
+ if ($progressCallback) {
+ $progressCallback($classNamesAndAlias['alias']);
+ }
$this->contentRepository->resetProjectionState($classNamesAndAlias['className']);
$this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options);
}
}
+ public function resetAllProjections(): void
+ {
+ foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) {
+ $this->contentRepository->resetProjectionState($classNamesAndAlias['className']);
+ }
+ }
+
/**
* @return class-string>
*/
@@ -80,6 +90,4 @@ private static function projectionAlias(string $className): string
}
return $alias;
}
-
-
}
diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php
index 0ba3ae6dd85..de0afa4aa3f 100644
--- a/Neos.Neos/Classes/Command/CrCommandController.php
+++ b/Neos.Neos/Classes/Command/CrCommandController.php
@@ -7,19 +7,18 @@
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
-use Neos\ContentRepository\Core\Service\ContentRepositoryBootstrapper;
+use Neos\ContentRepository\Core\Projection\CatchUpOptions;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Export\ExportService;
use Neos\ContentRepository\Export\ExportServiceFactory;
use Neos\ContentRepository\Export\ImportService;
use Neos\ContentRepository\Export\ImportServiceFactory;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
+use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory;
use Neos\Neos\AssetUsage\Projection\AssetUsageFinder;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;
-use Neos\Flow\Core\Booting\Scripts;
use Neos\Media\Domain\Repository\AssetRepository;
-use Neos\Neos\Domain\Service\NodeTypeNameFactory;
use Neos\Utility\Files;
use Neos\Flow\ResourceManagement\ResourceRepository;
use Neos\Flow\ResourceManagement\ResourceManager;
@@ -39,6 +38,7 @@ public function __construct(
private readonly ResourceManager $resourceManager,
private readonly PersistenceManagerInterface $persistenceManager,
private readonly ContentRepositoryRegistry $contentRepositoryRegistry,
+ private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory,
) {
parent::__construct();
}
@@ -109,7 +109,9 @@ public function importCommand(string $path, string $contentRepository = 'default
}
$this->outputLine('Replaying projections');
- Scripts::executeCommand('neos.contentrepositoryregistry:cr:replayall', $this->flowSettings, false, ['contentRepository' => $contentRepositoryId->value]);
+
+ $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory);
+ $projectionService->replayAllProjections(CatchUpOptions::create());
$this->outputLine('Done');
}