From b6787ba43870109d9f794211059bcd19f89f5fad Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 21 May 2024 08:56:13 +0200 Subject: [PATCH 01/26] BUGFIX Workspace aware NodeCacheEntryIdentifier --- Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php b/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php index 936e0c6e427..efbf48f7da2 100644 --- a/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php +++ b/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php @@ -32,7 +32,7 @@ private function __construct( public static function fromNode(Node $node): self { - return new self('Node_' . $node->subgraphIdentity->contentStreamId->value + return new self('Node_' . $node->workspaceName->value . '_' . $node->dimensionSpacePoint->hash . '_' . $node->aggregateId->value); } From c0ea2c071e0f828c87b3abddd7d6a7e56ba82dba Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 24 May 2024 13:20:05 +0200 Subject: [PATCH 02/26] BUGFIX: Reset projections after setup in `cr:setup` command Fixes: #5008 --- .../Classes/Command/CrCommandController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index b5d98b29e8e..7696dbbd19a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -44,6 +44,9 @@ public function setupCommand(string $contentRepository = 'default', bool $resetP { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $this->contentRepositoryRegistry->get($contentRepositoryId)->setUp(); + $this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]); + 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.'); @@ -54,9 +57,6 @@ public function setupCommand(string $contentRepository = 'default', bool $resetP $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]); } /** From 5a092d0536b6420e1a5f5af0bbe5a18e1f1b25df Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Sat, 25 May 2024 14:12:08 +0200 Subject: [PATCH 03/26] Remove `--reset-projections` flag --- .../Classes/Command/CrCommandController.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 7696dbbd19a..47ab58afc2a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -38,25 +38,13 @@ public function __construct( * 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', bool $resetProjections = false): void + public function setupCommand(string $contentRepository = 'default'): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $this->contentRepositoryRegistry->get($contentRepositoryId)->setUp(); $this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]); - - 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]); - } } /** From 0db90e304b93182cc89f2e46da825eb8ff6b7c5e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 26 May 2024 09:41:26 +0200 Subject: [PATCH 04/26] TASK: Move `NodeCacheEntryIdentifier` to Fusion namespace --- .../Cache}/NodeCacheEntryIdentifier.php | 2 +- Neos.Neos/Classes/Fusion/Helper/CachingHelper.php | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) rename Neos.Neos/Classes/{Domain/Model => Fusion/Cache}/NodeCacheEntryIdentifier.php (96%) diff --git a/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php b/Neos.Neos/Classes/Fusion/Cache/NodeCacheEntryIdentifier.php similarity index 96% rename from Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php rename to Neos.Neos/Classes/Fusion/Cache/NodeCacheEntryIdentifier.php index efbf48f7da2..40a9315a24f 100644 --- a/Neos.Neos/Classes/Domain/Model/NodeCacheEntryIdentifier.php +++ b/Neos.Neos/Classes/Fusion/Cache/NodeCacheEntryIdentifier.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\Neos\Domain\Model; +namespace Neos\Neos\Fusion\Cache; use Neos\Flow\Annotations as Flow; use Neos\Cache\CacheAwareInterface; diff --git a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php index 5901939576b..fb64a7a175b 100644 --- a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php @@ -22,7 +22,7 @@ use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Domain\Model\NodeCacheEntryIdentifier; +use Neos\Neos\Fusion\Cache\NodeCacheEntryIdentifier; use Neos\Neos\Fusion\Cache\CacheTag; use Neos\Neos\Fusion\Cache\CacheTagSet; @@ -56,6 +56,14 @@ public function nodeTag(iterable|Node $nodes): array return CacheTagSet::forNodeAggregatesFromNodes(Nodes::fromArray($nodes))->toStringArray(); } + /** + * Generate a `@cache` entry identifier for a given node: + * + * entryIdentifier { + * documentNode = ${Neos.Caching.entryIdentifierForNode(documentNode)} + * } + * + */ public function entryIdentifierForNode(Node $node): NodeCacheEntryIdentifier { return NodeCacheEntryIdentifier::fromNode($node); From 4cbe2a40361c43db46c371cb66116fccfe52849b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 27 May 2024 09:59:19 +0200 Subject: [PATCH 05/26] TASK: Use correct @template-covariant tag for ProjectionFactoryInterface See also https://github.com/neos/neos-development-collection/commit/329330e02b6b3aec50c1c3e041306f67ce59809d See https://phpstan.org/blog/whats-up-with-template-covariant --- .../Classes/Projection/ProjectionFactoryInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionFactoryInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionFactoryInterface.php index e1609e6e6be..6c0de0992d5 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionFactoryInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionFactoryInterface.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Factory\ProjectionFactoryDependencies; /** - * @template T of ProjectionInterface + * @template-covariant T of ProjectionInterface * @api */ interface ProjectionFactoryInterface From 596cb2fc13def5e0a84fe5ad5d59e9d1c0948778 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Mon, 3 Jun 2024 22:48:17 +0200 Subject: [PATCH 06/26] BUGFIX: Flush node content caches without workspace name --- Neos.Neos/Classes/Fusion/Cache/CacheTag.php | 45 ++++++++++++++++--- .../Classes/Fusion/Cache/CacheTagSet.php | 35 +++++++++++++++ .../Fusion/Cache/ContentCacheFlusher.php | 20 ++++++--- .../Classes/Fusion/Helper/CachingHelper.php | 28 ++++++++---- 4 files changed, 105 insertions(+), 23 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php index 69814d621ef..98b8e1dcabf 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php @@ -32,7 +32,7 @@ private function __construct( final public static function forNodeAggregate( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { return new self( @@ -51,9 +51,18 @@ final public static function forNodeAggregateFromNode(Node $node): self ); } + final public static function forNodeAggregateFromNodeWithoutWorkspace(Node $node): self + { + return self::forNodeAggregate( + $node->contentRepositoryId, + null, + $node->aggregateId + ); + } + final public static function forDescendantOfNode( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { return new self( @@ -72,9 +81,18 @@ final public static function forDescendantOfNodeFromNode(Node $node): self ); } + final public static function forDescendantOfNodeFromNodeWithoutWorkspace(Node $node): self + { + return self::forDescendantOfNode( + $node->contentRepositoryId, + null, + $node->aggregateId + ); + } + final public static function forAncestorNode( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { return new self( @@ -93,9 +111,18 @@ final public static function forAncestorNodeFromNode(Node $node): self ); } + final public static function forAncestorNodeFromNodeWithoutWorkspace(Node $node): self + { + return self::forAncestorNode( + $node->contentRepositoryId, + null, + $node->aggregateId + ); + } + final public static function forNodeTypeName( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeTypeName $nodeTypeName, ): self { return new self( @@ -107,7 +134,7 @@ final public static function forNodeTypeName( final public static function forDynamicNodeAggregate( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { return new self( @@ -123,9 +150,13 @@ final public static function fromString(string $string): self } protected static function getHashForWorkspaceNameAndContentRepositoryId( - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, ContentRepositoryId $contentRepositoryId, ): string { - return sha1($workspaceName->value . '@' . $contentRepositoryId->value); + if ($workspaceName) { + return sha1($workspaceName->value . '@' . $contentRepositoryId->value); + } + return sha1($contentRepositoryId->value); + } } diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php b/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php index b620b062470..e8e3ec2a603 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php @@ -45,6 +45,17 @@ public static function forDescendantOfNodesFromNodes( )); } + public static function forDescendantOfNodesFromNodesWithoutWorkspace( + Nodes $nodes + ): self { + return new self(...array_map( + fn (Node $node): CacheTag => CacheTag::forDescendantOfNodeFromNodeWithoutWorkspace( + $node + ), + iterator_to_array($nodes) + )); + } + public static function forNodeAggregatesFromNodes( Nodes $nodes ): self { @@ -56,6 +67,16 @@ public static function forNodeAggregatesFromNodes( )); } + public static function forNodeAggregatesFromNodesWithoutWorkspace( + Nodes $nodes + ): self { + return new self(...array_map( + fn (Node $node): CacheTag => CacheTag::forNodeAggregateFromNodeWithoutWorkspace( + $node + ), + iterator_to_array($nodes) + )); + } public static function forNodeTypeNames( ContentRepositoryId $contentRepositoryId, @@ -72,6 +93,20 @@ public static function forNodeTypeNames( )); } + public static function forNodeTypeNamesWithoutWorkspace( + ContentRepositoryId $contentRepositoryId, + NodeTypeNames $nodeTypeNames + ): self { + return new self(...array_map( + fn (NodeTypeName $nodeTypeName): CacheTag => CacheTag::forNodeTypeName( + $contentRepositoryId, + null, + $nodeTypeName + ), + iterator_to_array($nodeTypeNames) + )); + } + public function add(CacheTag $cacheTag): self { $tags = $this->tags; diff --git a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php index daea26cfeec..59e5908748f 100644 --- a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php +++ b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php @@ -72,7 +72,7 @@ public function flushNodeAggregate( $tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".'; $tagsToFlush = array_merge( - $this->collectTagsForChangeOnNodeAggregate($contentRepository, $workspaceName, $nodeAggregateId), + $this->collectTagsForChangeOnNodeAggregate($contentRepository, $workspaceName, $nodeAggregateId, $workspaceName), $tagsToFlush ); @@ -80,12 +80,17 @@ public function flushNodeAggregate( } /** + * WorkspaceNameToFlush is nullable, so we can flush all workspaces as no specific workspaceName is provided. + * This is needed to flush nodes on asset changes, as the asset can get rendered in all workspaces, but lives + * usually only in live workspace. + * * @return array */ private function collectTagsForChangeOnNodeAggregate( ContentRepository $contentRepository, WorkspaceName $workspaceName, - NodeAggregateId $nodeAggregateId + NodeAggregateId $nodeAggregateId, + ?WorkspaceName $workspaceNameToFlush ): array { $contentGraph = $contentRepository->getContentGraph($workspaceName); @@ -96,12 +101,12 @@ private function collectTagsForChangeOnNodeAggregate( // Node Aggregate was removed in the meantime, so no need to clear caches on this one anymore. return []; } - $tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $workspaceName, $nodeAggregateId); + $tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $workspaceNameToFlush, $nodeAggregateId); $tagsToFlush = array_merge($this->collectTagsForChangeOnNodeType( $nodeAggregate->nodeTypeName, $contentRepository->id, - $workspaceName, + $workspaceNameToFlush, $nodeAggregateId, $contentRepository ), $tagsToFlush); @@ -159,7 +164,7 @@ private function collectTagsForChangeOnNodeAggregate( */ private function collectTagsForChangeOnNodeIdentifier( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): array { $tagsToFlush = []; @@ -192,7 +197,7 @@ private function collectTagsForChangeOnNodeIdentifier( private function collectTagsForChangeOnNodeType( NodeTypeName $nodeTypeName, ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + ?WorkspaceName $workspaceName, ?NodeAggregateId $referenceNodeIdentifier, ContentRepository $contentRepository ): array { @@ -305,7 +310,8 @@ public function registerAssetChange(AssetInterface $asset): void $this->collectTagsForChangeOnNodeAggregate( $contentRepository, $workspaceName, - $usage->nodeAggregateId + $usage->nodeAggregateId, + null ), $tagsToFlush ); diff --git a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php index 5901939576b..c4d7485b7d0 100644 --- a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php @@ -53,7 +53,10 @@ public function nodeTag(iterable|Node $nodes): array $nodes = iterator_to_array($nodes); } - return CacheTagSet::forNodeAggregatesFromNodes(Nodes::fromArray($nodes))->toStringArray(); + return array_merge( + CacheTagSet::forNodeAggregatesFromNodes(Nodes::fromArray($nodes))->toStringArray(), + CacheTagSet::forNodeAggregatesFromNodesWithoutWorkspace(Nodes::fromArray($nodes))->toStringArray(), + ); } public function entryIdentifierForNode(Node $node): NodeCacheEntryIdentifier @@ -94,11 +97,17 @@ public function nodeTypeTag(string|iterable $nodeTypes, Node $contextNode): arra $nodeTypes = iterator_to_array($nodeTypes); } - return CacheTagSet::forNodeTypeNames( - $contextNode->contentRepositoryId, - $contextNode->workspaceName, - NodeTypeNames::fromStringArray($nodeTypes) - )->toStringArray(); + return array_merge( + CacheTagSet::forNodeTypeNames( + $contextNode->contentRepositoryId, + $contextNode->workspaceName, + NodeTypeNames::fromStringArray($nodeTypes) + )->toStringArray(), + CacheTagSet::forNodeTypeNamesWithoutWorkspace( + $contextNode->contentRepositoryId, + NodeTypeNames::fromStringArray($nodeTypes) + )->toStringArray(), + ); } /** @@ -118,9 +127,10 @@ public function descendantOfTag(iterable|Node $nodes): array $nodes = iterator_to_array($nodes); } - return CacheTagSet::forDescendantOfNodesFromNodes( - Nodes::fromArray($nodes) - )->toStringArray(); + return array_merge( + CacheTagSet::forDescendantOfNodesFromNodes(Nodes::fromArray($nodes))->toStringArray(), + CacheTagSet::forDescendantOfNodesFromNodesWithoutWorkspace(Nodes::fromArray($nodes))->toStringArray(), + ); } /** From 3614a5b84ab4e42d67855733cf5cace853a44cbe Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Mon, 3 Jun 2024 22:57:05 +0200 Subject: [PATCH 07/26] BUGFIX: Flush node content caches without workspace name --- Neos.Neos/Classes/Fusion/Cache/CacheTag.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php index 98b8e1dcabf..b471c458ade 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php @@ -157,6 +157,5 @@ protected static function getHashForWorkspaceNameAndContentRepositoryId( return sha1($workspaceName->value . '@' . $contentRepositoryId->value); } return sha1($contentRepositoryId->value); - } } From 36e8c505a0c2626b28cc6df91cf33477fcb4cff5 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 4 Jun 2024 00:26:17 +0200 Subject: [PATCH 08/26] BUGFIX: Flush node content caches without workspace name --- .../Unit/Fusion/Helper/CachingHelperTest.php | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php index 2f9916b6399..3b9992926f9 100644 --- a/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/Helper/CachingHelperTest.php @@ -57,12 +57,20 @@ public function nodeTypeTagDataProvider() $nodeTypeName3 = 'Neos.Neos:Moo'; return [ - [$nodeTypeName1, ['NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Foo']], + [$nodeTypeName1, + [ + 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Foo', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Foo', + ] + ], [[$nodeTypeName1, $nodeTypeName2, $nodeTypeName3], [ 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Foo', 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Bar', 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Moo', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Foo', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Bar', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Moo', ] ], [(new \ArrayObject([$nodeTypeName1, $nodeTypeName2, $nodeTypeName3])), @@ -70,6 +78,9 @@ public function nodeTypeTagDataProvider() 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Foo', 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Bar', 'NodeType_364cfc8e70b2baa23dbd14503d2bd00e063829e7_Neos_Neos-Moo', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Foo', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Bar', + 'NodeType_7505d64a54e061b7acd54ccd58b49dc43500b635_Neos_Neos-Moo', ] ], ]; @@ -103,15 +114,25 @@ public function nodeDataProvider() $node2 = $this->createNode(NodeAggregateId::fromString($nodeIdentifier2)); return [ - [$node1, ['Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306']], - [[$node1], ['Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306']], + [$node1, [ + 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + ]], + [[$node1], [ + 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + ]], [[$node1, $node2], [ 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', - 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a' + 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_7005c7cf-4d19-ce36-0873-476b6cadb71a', ]], [(new \ArrayObject([$node1, $node2])), [ 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', - 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a' + 'Node_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'Node_7505d64a54e061b7acd54ccd58b49dc43500b635_7005c7cf-4d19-ce36-0873-476b6cadb71a', ]] ]; } @@ -169,15 +190,25 @@ public function descendantOfDataProvider() return [ - [$node1, ['DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306']], - [[$node1], ['DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306']], + [$node1, [ + 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + ]], + [[$node1], [ + 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + ]], [[$node1, $node2], [ 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', - 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a' + 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_7005c7cf-4d19-ce36-0873-476b6cadb71a', ]], [(new \ArrayObject([$node1, $node2])), [ 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_ca511a55-c5c0-f7d7-8d71-8edeffc75306', - 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a' + 'DescendantOf_364cfc8e70b2baa23dbd14503d2bd00e063829e7_7005c7cf-4d19-ce36-0873-476b6cadb71a', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_ca511a55-c5c0-f7d7-8d71-8edeffc75306', + 'DescendantOf_7505d64a54e061b7acd54ccd58b49dc43500b635_7005c7cf-4d19-ce36-0873-476b6cadb71a', ]] ]; } From 8b8e4f4e3d60d2d5f77c100958bcab831ab94b10 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 4 Jun 2024 10:11:38 +0200 Subject: [PATCH 09/26] BUGFIX: Flush node content caches without workspace name --- Neos.Neos/Classes/Fusion/Cache/CacheTag.php | 41 +++++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php index b471c458ade..1c0eb7867f7 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php @@ -18,6 +18,8 @@ class CacheTag { protected const PATTERN = '/^[a-zA-Z0-9_%\-&]{1,250}$/'; + protected const NODE_PREFIX = 'Node'; + protected const PARTS_DELIMITER = '_'; private function __construct( public readonly string $value @@ -32,13 +34,28 @@ private function __construct( final public static function forNodeAggregate( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { return new self( - 'Node_' + self::NODE_PREFIX + . self::PARTS_DELIMITER . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . '_' . $nodeAggregateId->value + . self::PARTS_DELIMITER + . $nodeAggregateId->value + ); + } + + final public static function forNodeAggregateWithoutWorkspaceName( + ContentRepositoryId $contentRepositoryId, + NodeAggregateId $nodeAggregateId, + ): self { + return new self( + self::NODE_PREFIX + . self::PARTS_DELIMITER + . self::getHashForContentRepositoryId($contentRepositoryId) + . self::PARTS_DELIMITER + . $nodeAggregateId->value ); } @@ -53,9 +70,8 @@ final public static function forNodeAggregateFromNode(Node $node): self final public static function forNodeAggregateFromNodeWithoutWorkspace(Node $node): self { - return self::forNodeAggregate( + return self::forNodeAggregateWithoutWorkspaceName( $node->contentRepositoryId, - null, $node->aggregateId ); } @@ -83,9 +99,8 @@ final public static function forDescendantOfNodeFromNode(Node $node): self final public static function forDescendantOfNodeFromNodeWithoutWorkspace(Node $node): self { - return self::forDescendantOfNode( + return self::forNodeAggregateWithoutWorkspaceName( $node->contentRepositoryId, - null, $node->aggregateId ); } @@ -113,9 +128,8 @@ final public static function forAncestorNodeFromNode(Node $node): self final public static function forAncestorNodeFromNodeWithoutWorkspace(Node $node): self { - return self::forAncestorNode( + return self::forNodeAggregateWithoutWorkspaceName( $node->contentRepositoryId, - null, $node->aggregateId ); } @@ -153,9 +167,12 @@ protected static function getHashForWorkspaceNameAndContentRepositoryId( ?WorkspaceName $workspaceName, ContentRepositoryId $contentRepositoryId, ): string { - if ($workspaceName) { - return sha1($workspaceName->value . '@' . $contentRepositoryId->value); - } + return sha1($workspaceName->value . '@' . $contentRepositoryId->value); + } + + protected static function getHashForContentRepositoryId( + ContentRepositoryId $contentRepositoryId, + ): string { return sha1($contentRepositoryId->value); } } From 2a6a8ccc81b704830efe5240205b118c1945a986 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Tue, 4 Jun 2024 14:33:55 +0200 Subject: [PATCH 10/26] Adjust `CacheTag` to use special `CacheTagWorkspaceName::ANY` instead of `null` --- Neos.Neos/Classes/Fusion/Cache/CacheTag.php | 111 ++++++------------ .../Classes/Fusion/Cache/CacheTagSet.php | 46 +++----- .../Fusion/Cache/CacheTagWorkspaceName.php | 13 ++ .../Fusion/Cache/ContentCacheFlusher.php | 11 +- .../Classes/Fusion/Helper/CachingHelper.php | 4 +- 5 files changed, 76 insertions(+), 109 deletions(-) create mode 100644 Neos.Neos/Classes/Fusion/Cache/CacheTagWorkspaceName.php diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php index 1c0eb7867f7..40510f233ba 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTag.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTag.php @@ -18,8 +18,11 @@ class CacheTag { protected const PATTERN = '/^[a-zA-Z0-9_%\-&]{1,250}$/'; - protected const NODE_PREFIX = 'Node'; - protected const PARTS_DELIMITER = '_'; + protected const PREFIX_NODE = 'Node'; + protected const PREFIX_DESCENDANT_OF = 'DescendantOf'; + protected const PREFIX_ANCESTOR = 'Ancestor'; + protected const PREFIX_NODE_TYPE = 'NodeType'; + protected const PREFIX_DYNAMIC_NODE_TAG = 'DynamicNodeTag'; private function __construct( public readonly string $value @@ -32,30 +35,20 @@ private function __construct( } } - final public static function forNodeAggregate( - ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, - NodeAggregateId $nodeAggregateId, - ): self { - return new self( - self::NODE_PREFIX - . self::PARTS_DELIMITER - . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . self::PARTS_DELIMITER - . $nodeAggregateId->value - ); + private static function fromSegments(string ...$segments): self + { + return new self(implode('_', $segments)); } - final public static function forNodeAggregateWithoutWorkspaceName( + final public static function forNodeAggregate( ContentRepositoryId $contentRepositoryId, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { - return new self( - self::NODE_PREFIX - . self::PARTS_DELIMITER - . self::getHashForContentRepositoryId($contentRepositoryId) - . self::PARTS_DELIMITER - . $nodeAggregateId->value + return self::fromSegments( + self::PREFIX_NODE, + self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId), + $nodeAggregateId->value, ); } @@ -68,23 +61,15 @@ final public static function forNodeAggregateFromNode(Node $node): self ); } - final public static function forNodeAggregateFromNodeWithoutWorkspace(Node $node): self - { - return self::forNodeAggregateWithoutWorkspaceName( - $node->contentRepositoryId, - $node->aggregateId - ); - } - final public static function forDescendantOfNode( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { - return new self( - 'DescendantOf_' - . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . '_' . $nodeAggregateId->value + return self::fromSegments( + self::PREFIX_DESCENDANT_OF, + self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId), + $nodeAggregateId->value, ); } @@ -97,23 +82,15 @@ final public static function forDescendantOfNodeFromNode(Node $node): self ); } - final public static function forDescendantOfNodeFromNodeWithoutWorkspace(Node $node): self - { - return self::forNodeAggregateWithoutWorkspaceName( - $node->contentRepositoryId, - $node->aggregateId - ); - } - final public static function forAncestorNode( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { - return new self( - 'Ancestor_' - . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . '_' . $nodeAggregateId->value + return self::fromSegments( + self::PREFIX_ANCESTOR, + self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId), + $nodeAggregateId->value, ); } @@ -126,35 +103,27 @@ final public static function forAncestorNodeFromNode(Node $node): self ); } - final public static function forAncestorNodeFromNodeWithoutWorkspace(Node $node): self - { - return self::forNodeAggregateWithoutWorkspaceName( - $node->contentRepositoryId, - $node->aggregateId - ); - } - final public static function forNodeTypeName( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeTypeName $nodeTypeName, ): self { - return new self( - 'NodeType_' - . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . '_' . \strtr($nodeTypeName->value, '.:', '_-') + return self::fromSegments( + self::PREFIX_NODE_TYPE, + self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId), + \strtr($nodeTypeName->value, '.:', '_-'), ); } final public static function forDynamicNodeAggregate( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): self { - return new self( - 'DynamicNodeTag_' - . self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId) - . '_' . $nodeAggregateId->value + return self::fromSegments( + self::PREFIX_DYNAMIC_NODE_TAG, + self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId), + $nodeAggregateId->value, ); } @@ -164,15 +133,11 @@ final public static function fromString(string $string): self } protected static function getHashForWorkspaceNameAndContentRepositoryId( - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, ContentRepositoryId $contentRepositoryId, ): string { - return sha1($workspaceName->value . '@' . $contentRepositoryId->value); - } - - protected static function getHashForContentRepositoryId( - ContentRepositoryId $contentRepositoryId, - ): string { - return sha1($contentRepositoryId->value); + return sha1( + $workspaceName === CacheTagWorkspaceName::ANY ? $contentRepositoryId->value : $workspaceName->value . '@' . $contentRepositoryId->value + ); } } diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php b/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php index e8e3ec2a603..e96bfd769cb 100644 --- a/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php @@ -38,19 +38,19 @@ public static function forDescendantOfNodesFromNodes( Nodes $nodes ): self { return new self(...array_map( - fn (Node $node): CacheTag => CacheTag::forDescendantOfNodeFromNode( - $node - ), - iterator_to_array($nodes) + CacheTag::forDescendantOfNodeFromNode(...), + iterator_to_array($nodes), )); } public static function forDescendantOfNodesFromNodesWithoutWorkspace( - Nodes $nodes + Nodes $nodes, ): self { return new self(...array_map( - fn (Node $node): CacheTag => CacheTag::forDescendantOfNodeFromNodeWithoutWorkspace( - $node + static fn (Node $node) => CacheTag::forDescendantOfNode( + $node->contentRepositoryId, + CacheTagWorkspaceName::ANY, + $node->aggregateId, ), iterator_to_array($nodes) )); @@ -60,9 +60,7 @@ public static function forNodeAggregatesFromNodes( Nodes $nodes ): self { return new self(...array_map( - fn (Node $node): CacheTag => CacheTag::forNodeAggregateFromNode( - $node - ), + CacheTag::forNodeAggregateFromNode(...), iterator_to_array($nodes) )); } @@ -71,20 +69,22 @@ public static function forNodeAggregatesFromNodesWithoutWorkspace( Nodes $nodes ): self { return new self(...array_map( - fn (Node $node): CacheTag => CacheTag::forNodeAggregateFromNodeWithoutWorkspace( - $node + static fn (Node $node) => CacheTag::forNodeAggregate( + $node->contentRepositoryId, + CacheTagWorkspaceName::ANY, + $node->aggregateId ), - iterator_to_array($nodes) + iterator_to_array($nodes), )); } public static function forNodeTypeNames( ContentRepositoryId $contentRepositoryId, - WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeTypeNames $nodeTypeNames ): self { return new self(...array_map( - fn (NodeTypeName $nodeTypeName): CacheTag => CacheTag::forNodeTypeName( + static fn (NodeTypeName $nodeTypeName): CacheTag => CacheTag::forNodeTypeName( $contentRepositoryId, $workspaceName, $nodeTypeName @@ -93,20 +93,6 @@ public static function forNodeTypeNames( )); } - public static function forNodeTypeNamesWithoutWorkspace( - ContentRepositoryId $contentRepositoryId, - NodeTypeNames $nodeTypeNames - ): self { - return new self(...array_map( - fn (NodeTypeName $nodeTypeName): CacheTag => CacheTag::forNodeTypeName( - $contentRepositoryId, - null, - $nodeTypeName - ), - iterator_to_array($nodeTypeNames) - )); - } - public function add(CacheTag $cacheTag): self { $tags = $this->tags; @@ -121,7 +107,7 @@ public function add(CacheTag $cacheTag): self public function toStringArray(): array { return array_map( - fn (CacheTag $tag): string => $tag->value, + static fn (CacheTag $tag): string => $tag->value, array_values($this->tags) ); } diff --git a/Neos.Neos/Classes/Fusion/Cache/CacheTagWorkspaceName.php b/Neos.Neos/Classes/Fusion/Cache/CacheTagWorkspaceName.php new file mode 100644 index 00000000000..2aaa79b476e --- /dev/null +++ b/Neos.Neos/Classes/Fusion/Cache/CacheTagWorkspaceName.php @@ -0,0 +1,13 @@ +collectTagsForChangeOnNodeAggregate($contentRepository, $workspaceName, $nodeAggregateId, $workspaceName), + $this->collectTagsForChangeOnNodeAggregate($contentRepository, $workspaceName, $nodeAggregateId, false), $tagsToFlush ); @@ -90,7 +90,7 @@ private function collectTagsForChangeOnNodeAggregate( ContentRepository $contentRepository, WorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, - ?WorkspaceName $workspaceNameToFlush + bool $anyWorkspace, ): array { $contentGraph = $contentRepository->getContentGraph($workspaceName); @@ -101,6 +101,7 @@ private function collectTagsForChangeOnNodeAggregate( // Node Aggregate was removed in the meantime, so no need to clear caches on this one anymore. return []; } + $workspaceNameToFlush = $anyWorkspace ? CacheTagWorkspaceName::ANY : $workspaceName; $tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $workspaceNameToFlush, $nodeAggregateId); $tagsToFlush = array_merge($this->collectTagsForChangeOnNodeType( @@ -164,7 +165,7 @@ private function collectTagsForChangeOnNodeAggregate( */ private function collectTagsForChangeOnNodeIdentifier( ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, NodeAggregateId $nodeAggregateId, ): array { $tagsToFlush = []; @@ -197,7 +198,7 @@ private function collectTagsForChangeOnNodeIdentifier( private function collectTagsForChangeOnNodeType( NodeTypeName $nodeTypeName, ContentRepositoryId $contentRepositoryId, - ?WorkspaceName $workspaceName, + WorkspaceName|CacheTagWorkspaceName $workspaceName, ?NodeAggregateId $referenceNodeIdentifier, ContentRepository $contentRepository ): array { @@ -311,7 +312,7 @@ public function registerAssetChange(AssetInterface $asset): void $contentRepository, $workspaceName, $usage->nodeAggregateId, - null + true ), $tagsToFlush ); diff --git a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php index c4d7485b7d0..11e56aa3d26 100644 --- a/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/CachingHelper.php @@ -25,6 +25,7 @@ use Neos\Neos\Domain\Model\NodeCacheEntryIdentifier; use Neos\Neos\Fusion\Cache\CacheTag; use Neos\Neos\Fusion\Cache\CacheTagSet; +use Neos\Neos\Fusion\Cache\CacheTagWorkspaceName; /** * Caching helper to make cache tag generation easier. @@ -103,8 +104,9 @@ public function nodeTypeTag(string|iterable $nodeTypes, Node $contextNode): arra $contextNode->workspaceName, NodeTypeNames::fromStringArray($nodeTypes) )->toStringArray(), - CacheTagSet::forNodeTypeNamesWithoutWorkspace( + CacheTagSet::forNodeTypeNames( $contextNode->contentRepositoryId, + CacheTagWorkspaceName::ANY, NodeTypeNames::fromStringArray($nodeTypes) )->toStringArray(), ); From 2b4890cb5596cab3e921bdc027b01376c4c11bb9 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Tue, 4 Jun 2024 14:51:20 +0200 Subject: [PATCH 11/26] BUGFIX: Flush node content caches without workspace name --- Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php index 19ddc3fffbf..992d776e12d 100644 --- a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php +++ b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php @@ -80,9 +80,8 @@ public function flushNodeAggregate( } /** - * WorkspaceNameToFlush is nullable, so we can flush all workspaces as no specific workspaceName is provided. - * This is needed to flush nodes on asset changes, as the asset can get rendered in all workspaces, but lives - * usually only in live workspace. + * @param bool $anyWorkspace This is needed to flush nodes on asset changes, as the asset can get rendered in all workspaces, but lives + * usually only in live workspace. * * @return array */ From d27a607a670fde981cfb4c216bab4fbc25e025ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anke=20H=C3=A4slich?= Date: Mon, 10 Jun 2024 13:14:29 +0200 Subject: [PATCH 12/26] TASK: Remove generation of site.xml in `./flow kickstart:site` As the `./flow site:import` command was removed we don't need to generate a `site.xml` anymore. --- .../Command/KickstartCommandController.php | 2 +- .../Generator/AfxTemplateGenerator.php | 28 +---------------- .../Private/AfxGenerator/Content/Sites.xml | 31 ------------------- 3 files changed, 2 insertions(+), 59 deletions(-) delete mode 100755 Neos.SiteKickstarter/Resources/Private/AfxGenerator/Content/Sites.xml diff --git a/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php b/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php index a294af48583..1e9dabe5cd2 100755 --- a/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php +++ b/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php @@ -44,7 +44,7 @@ class KickstartCommandController extends CommandController /** * Kickstart a new site package * - * This command generates a new site package with basic Fusion and Sites.xml + * This command generates a new site package with basic Fusion * * @param string $packageKey The packageKey for your site * @param string $siteName The siteName of your site diff --git a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php index a9d6687f9ea..e9aa151cde5 100755 --- a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php +++ b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php @@ -17,9 +17,9 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Package\PackageManager; use Neos\Kickstarter\Service\GeneratorService; +use Neos\SiteKickstarter\Service\FusionRecursiveDirectoryRenderer; use Neos\SiteKickstarter\Service\SimpleTemplateRenderer; use Neos\Utility\Files; -use Neos\SiteKickstarter\Service\FusionRecursiveDirectoryRenderer; /** * Service to generate site packages @@ -63,7 +63,6 @@ public function generateSitePackage(string $packageKey, string $siteName) : arra ] ]); - $this->generateSitesXml($packageKey, $siteName); $this->generateSitesFusionDirectory($packageKey, $siteName); $this->generateNodeTypesConfiguration($packageKey); $this->generateAdditionalFolders($packageKey); @@ -71,31 +70,6 @@ public function generateSitePackage(string $packageKey, string $siteName) : arra return $this->generatedFiles; } - /** - * Generate a "Sites.xml" for the given package and name. - * - * @param string $packageKey - * @param string $siteName - * @throws \Neos\Flow\Package\Exception\UnknownPackageException - * @throws \Neos\FluidAdaptor\Core\Exception - */ - protected function generateSitesXml(string $packageKey, string $siteName) : void - { - $templatePathAndFilename = $this->getResourcePathForFile('Content/Sites.xml'); - - $contextVariables = [ - 'packageKey' => $packageKey, - 'siteName' => htmlspecialchars($siteName), - 'siteNodeName' => $this->generateSiteNodeName($packageKey), - 'dimensions' => 'wat' //$this->contentDimensionZookeeper->getAllowedDimensionSubspace() - ]; - - $fileContent = $this->renderTemplate($templatePathAndFilename, $contextVariables); - - $sitesXmlPathAndFilename = $this->packageManager->getPackage($packageKey)->getResourcesPath() . 'Private/Content/Sites.xml'; - $this->generateFile($sitesXmlPathAndFilename, $fileContent); - } - /** * Generate basic root Fusion file. * diff --git a/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Content/Sites.xml b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Content/Sites.xml deleted file mode 100755 index dcba85e68f6..00000000000 --- a/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Content/Sites.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - From 6e2bc3ce19d3b79cf33f7162d67b330356154ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anke=20H=C3=A4slich?= Date: Mon, 10 Jun 2024 13:21:23 +0200 Subject: [PATCH 13/26] TASK: Remove unused functions from AfxTemplateGenerator --- .../Generator/AfxTemplateGenerator.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php index e9aa151cde5..dc6118b3b04 100755 --- a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php +++ b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php @@ -70,29 +70,6 @@ public function generateSitePackage(string $packageKey, string $siteName) : arra return $this->generatedFiles; } - /** - * Generate basic root Fusion file. - * - * @param string $packageKey - * @param string $siteName - * @throws \Neos\Flow\Package\Exception\UnknownPackageException - */ - protected function generateSitesRootFusion(string $packageKey, string $siteName) : void - { - $templatePathAndFilename = $this->getResourcePathForFile('Fusion/Root.fusion'); - - $contextVariables = [ - 'packageKey' => $packageKey, - 'siteName' => $siteName, - 'siteNodeName' => $this->generateSiteNodeName($packageKey) - ]; - - $fileContent = $this->simpleTemplateRenderer->render($templatePathAndFilename, $contextVariables); - - $sitesRootFusionPathAndFilename = $this->packageManager->getPackage($packageKey)->getResourcesPath() . 'Private/Fusion/Root.fusion'; - $this->generateFile($sitesRootFusionPathAndFilename, $fileContent); - } - /** * Render the whole directory of the fusion part * @@ -119,14 +96,6 @@ protected function generateSitesFusionDirectory(string $packageKey, string $site ); } - /** - * Generate site node name based on the given package key - */ - protected function generateSiteNodeName(string $packageKey) : string - { - return NodeName::transliterateFromString($packageKey)->value; - } - /** * Generate a example NodeTypes.yaml * From 6c0678ec6de0b10054069a7cb12e386d5a542f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anke=20H=C3=A4slich?= Date: Mon, 10 Jun 2024 14:54:25 +0200 Subject: [PATCH 14/26] TASK: Create a custom document node type for the site root node Relates: neos/neos-development-collection#4563 Relates: neos/neos-development-collection#4567 --- .../Generator/AfxTemplateGenerator.php | 28 +++++++------------ .../Fusion/Document/Homepage.fusion | 1 + .../NodeTypes/Document.Homepage.yaml | 11 ++++++++ .../Document.Page.yaml} | 0 4 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 Neos.SiteKickstarter/Resources/Private/AfxGenerator/Fusion/Document/Homepage.fusion create mode 100644 Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Homepage.yaml rename Neos.SiteKickstarter/Resources/Private/AfxGenerator/{Configuration/NodeTypes.Document.Page.yaml => NodeTypes/Document.Page.yaml} (100%) diff --git a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php index dc6118b3b04..78551df7222 100755 --- a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php +++ b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php @@ -13,7 +13,6 @@ * source code. */ -use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\Flow\Annotations as Flow; use Neos\Flow\Package\PackageManager; use Neos\Kickstarter\Service\GeneratorService; @@ -86,11 +85,8 @@ protected function generateSitesFusionDirectory(string $packageKey, string $site $contextVariables['siteNodeName'] = $packageKeyDomainPart; $fusionRecursiveDirectoryRenderer = new FusionRecursiveDirectoryRenderer(); - - $packageDirectory = $this->packageManager->getPackage('Neos.SiteKickstarter')->getResourcesPath(); - $fusionRecursiveDirectoryRenderer->renderDirectory( - $packageDirectory . 'Private/AfxGenerator/Fusion', + $this->getTemplateFolder() . 'Fusion', $this->packageManager->getPackage($packageKey)->getResourcesPath() . 'Private/Fusion', $contextVariables ); @@ -104,16 +100,18 @@ protected function generateSitesFusionDirectory(string $packageKey, string $site */ protected function generateNodeTypesConfiguration(string $packageKey) : void { - $templatePathAndFilename = $this->getResourcePathForFile('Configuration/NodeTypes.Document.Page.yaml'); + $templateFolder = $this->getTemplateFolder() . 'NodeTypes'; + $targetFolder = $this->packageManager->getPackage($packageKey)->getPackagePath() . 'NodeTypes'; $contextVariables = [ 'packageKey' => $packageKey ]; - $fileContent = $this->simpleTemplateRenderer->render($templatePathAndFilename, $contextVariables); - - $sitesNodeTypesPathAndFilename = $this->packageManager->getPackage($packageKey)->getConfigurationPath() . 'NodeTypes.Document.Page.yaml'; - $this->generateFile($sitesNodeTypesPathAndFilename, $fileContent); + foreach (Files::readDirectoryRecursively($templateFolder, '.yaml') as $templatePathAndFilename) { + $fileContent = $this->simpleTemplateRenderer->render($templatePathAndFilename, $contextVariables); + $targetPathAndFilename = str_replace($templateFolder, $targetFolder, $templatePathAndFilename); + $this->generateFile($targetPathAndFilename, $fileContent); + } } /** @@ -133,15 +131,9 @@ protected function generateAdditionalFolders(string $packageKey) : void } } - /** - * returns resource path for the generator - * - * @param $pathToFile - * @return string - */ - protected function getResourcePathForFile(string $pathToFile) : string + protected function getTemplateFolder(): string { - return 'resource://Neos.SiteKickstarter/Private/AfxGenerator/' . $pathToFile; + return $this->packageManager->getPackage('Neos.SiteKickstarter')->getResourcesPath() . 'Private/AfxGenerator/'; } public function getGeneratorName(): string diff --git a/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Fusion/Document/Homepage.fusion b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Fusion/Document/Homepage.fusion new file mode 100644 index 00000000000..8737907cfcf --- /dev/null +++ b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Fusion/Document/Homepage.fusion @@ -0,0 +1 @@ +prototype({packageKey}:Document.Homepage) < prototype({packageKey}:Document.AbstractPage) diff --git a/Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Homepage.yaml b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Homepage.yaml new file mode 100644 index 00000000000..a51a0e66de7 --- /dev/null +++ b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Homepage.yaml @@ -0,0 +1,11 @@ +# This is a custom root node type which has been auto-generated for your site package +# by Neos. You can customize this to your needs. +'{packageKey}:Document.Homepage': + superTypes: + 'Neos.Neos:Site': true + ui: + icon: globe + label: '{packageKey} Homepage' + childNodes: + main: + type: 'Neos.Neos:ContentCollection' diff --git a/Neos.SiteKickstarter/Resources/Private/AfxGenerator/Configuration/NodeTypes.Document.Page.yaml b/Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Page.yaml similarity index 100% rename from Neos.SiteKickstarter/Resources/Private/AfxGenerator/Configuration/NodeTypes.Document.Page.yaml rename to Neos.SiteKickstarter/Resources/Private/AfxGenerator/NodeTypes/Document.Page.yaml From 82d54ce9a8f98fffbe4e7988acd654e7234a4b8c Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Tue, 11 Jun 2024 17:40:17 +0200 Subject: [PATCH 15/26] TASK: Tweak README This converts the `Readme.rst` to Markdown and splits it into two separate files, `README.md` and `CONTRIBUTING.md` as is convention these days. --- CONTRIBUTING.md | 134 +++++++++++++++++++++++++++++++ README.md | 93 ++++++++++++++++++++++ Readme.rst | 208 ------------------------------------------------ 3 files changed, 227 insertions(+), 208 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 README.md delete mode 100644 Readme.rst diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..0ff168555fc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,134 @@ +# Contributing + +**For (specific) Notes on Neos 9, see further down…** + +If you want to contribute to Neos and want to set up a development environment, then follow these steps: + +```shell +composer create-project neos/neos-development-distribution neos-development 8.3.x-dev --keep-vcs +``` + +Note the **-distribution** repository you create a project from, instead of just checking out this repository. + +If you need a different branch, you can either use it from the start (replace the ``8.3.x-dev`` by ``9.0.x-dev`` or whatever you need), or switch after checkout (just make sure to run composer update afterwards to get matching dependencies installed.) In a nutshell, to switch the branch you intend to work on, run: + +```shell +git checkout 9.0 && composer update +``` + +The code of the CMS can then be found inside `Packages/Neos`, which itself is the neos-development-collection Git repository. You commit changes and create pull requests from this repository. + +- To commit changes to Neos switch into the `Neos` directory (`cd Packages/Neos`) and do all Git-related work (`git add .`, `git commit`, etc) there. +- If you want to contribute to the Neos UI, please take a look at the explanations at https://github.com/neos/neos-ui#contributing on how to work with that. +- If you want to contribute to the Flow Framework, you find that inside the `Packages/Framework` folder. See https://github.com/neos/flow-development-collection + +In the root directory of the development distribution, you can do the following things: + +To run tests, run `./bin/phpunit -c ./Build/BuildEssentials/PhpUnit/UnitTests.xml` for unit tests or `./bin/phpunit -c ./Build/BuildEssentials/PhpUnit/FunctionalTests.xml` for functional/integration tests. + +--- + +**Note** + +We use an upmerging strategy: create all bugfixes to the lowest maintained branch that contains the issue. Typically, this is the second last LTS release - see the diagram at https://www.neos.io/features/release-process.html. + + For new features, pull requests should be made against the branch for the next minor version (named like `x.y`). Breaking changes must only go into the branch for the next major version. + +--- + +For more detailed information, see https://discuss.neos.io/t/development-setup/504, +https://discuss.neos.io/t/creating-a-pull-request/506 and +https://discuss.neos.io/t/git-branch-handling-in-the-neos-project/6013 + + +## Neos 9 + + +### Prerequisites + +- You need PHP 8.2 installed. +- Please be sure to run off the Neos development distribution in branch 9.0, to avoid dependency issues. + +### Setup + +The Event-Sourced Content Repository has a Docker Compose file included in `Neos.ContentRepository.BehavioralTests` which starts both Mariadb and Postgres in compatible versions. Additionally, there exists a helper to change the configuration in your distribution (`/Configuration`) to the correct values matching this database. + +Do the following for setting up everything: + +1. Start the databases: + + ```shell + pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose up -d; popd + + # to stop the databases: + pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose down; popd + # to stop the databases AND remove the stored data: + pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose down -v; popd + ``` + +2. Copy matching Configuration: + + ```shell + cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionConfigurationTemplate/* Configuration/ + ``` + +3. Run Doctrine Migrations: + + ```shell + ./flow doctrine:migrate + FLOW_CONTEXT=Testing/Postgres ./flow doctrine:migrate + ``` + +4. Setup the Content Repository + + ```shell + ./flow cr:setup + ``` + +5. Set up Behat + + ```shell + cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionBehatTemplate/ Build/Behat + pushd Build/Behat/ + rm composer.lock + composer install + popd + ``` + +### Running the Tests + +The normal mode is running PHP locally, but running Mariadb / Postgres in containers (so we know +we use the right versions etc). + +```shell +bin/behat -c Packages/Neos/Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist +``` + +Running all tests can take a long time, depending on the hardware. +To speed up the process, Behat tests can be executed in a "synchronous" mode by setting the `CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION` environment variable: + +```shell +CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 bin/behat -c Packages/Neos/Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist +``` + +Alternatively, if you want to reproduce errors as they happen inside the CI system, but you +develop on Mac OS, you might want to run the Behat tests in a Docker container (= a linux environment) +as well. We have seen cases where they behave differently, i.e. where they run without race +conditions on OSX, but with race conditions in Linux/Docker. Additionally, the Linux/Docker setup +described below also makes it easy to run the race-condition-detector: + +```shell +docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml build +docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml up -d +docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml run neos /bin/bash + +# the following commands now run INSIDE the Neos docker container: +FLOW_CONTEXT=Testing/Behat ../../../../../flow raceConditionTracker:reset + +../../../../../bin/behat -c behat.yml.dist + +# To run tests in speed mode, run CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 +CATCHUPTRIGGER_ENABLE_SYNCHRONOUS_OPTION=1 ../../../../../bin/behat -c behat.yml.dist + +FLOW_CONTEXT=Testing/Behat ../../../../../flow raceConditionTracker:analyzeTrace +``` diff --git a/README.md b/README.md new file mode 100644 index 00000000000..b7cb5af08a2 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +[![Code +Climate](https://codeclimate.com/github/neos/neos-development-collection/badges/gpa.svg)](https://codeclimate.com/github/neos/neos-development-collection) +[![StyleCI](https://styleci.io/repos/40964014/shield?style=flat)](https://styleci.io/repos/40964014) +[![Latest Stable +Version](https://poser.pugx.org/neos/neos-development-collection/v/9.0)](https://packagist.org/packages/neos/neos-development-collection) +[![License](https://poser.pugx.org/neos/neos-development-collection/license)](https://raw.githubusercontent.com/neos/neos-development-collection/9.0/LICENSE) +[![Documentation](https://img.shields.io/badge/documentation-master-blue.svg)](https://neos.readthedocs.org/en/9.0/) +[![Slack](http://slack.neos.io/badge.svg)](http://slack.neos.io) +[![Discussion Forum](https://img.shields.io/badge/forum-Discourse-39c6ff.svg)](https://discuss.neos.io/) +[![Issues](https://img.shields.io/github/issues/neos/neos-development-collection.svg)](https://github.com/neos/neos-development-collection/issues) +[![Translation](https://img.shields.io/badge/translate-weblate-85ae52.svg)](https://hosted.weblate.org/projects/neos/) +[![Twitter](https://img.shields.io/twitter/follow/neoscms.svg?style=social)](https://twitter.com/NeosCMS) + +# Neos development collection + +This repository is a collection of packages for the Neos content +application platform (learn more on ). The +repository is used for development and all pull requests should go into +it. + +## Installation and Setup + +If you want to install Neos, please have a look at the installation & +setup documentation: + + +**For (specific) documentation on Neos 9, read on below...** + +## Contributing + +If you want to contribute to Neos and want to set up a development +environment, then please read the instructions in [CONTRIBUTING.md](CONTRIBUTING.md) + +**For (specific) documentation on Neos 9, read on below...** + +## Neos 9 and the Event-Sourced Content Repository (ES CR) + +### Prerequisites + +- You need PHP 8.2 installed. +- Please be sure to run off the Neos development distribution in branch 9.0, to avoid dependency issues. + +### Setup + +Follow the usual configuration steps (as for Neos 8) to install Composer dependencies and configure the database connection in `Settings.yaml` Then: + +1. Run Doctrine Migrations: + + ``` bash + ./flow doctrine:migrate + FLOW_CONTEXT=Testing/Postgres ./flow doctrine:migrate + ``` + +2. Setup the Content Repository + + ``` bash + ./flow cr:setup + ``` + +### Site Setup + +You can chose from one of the following options: + +#### Creating a new Site + +``` bash +./flow site:create neosdemo Neos.Demo Neos.Demo:Document.Homepage +``` + +#### Migrating an existing (Neos < 9.0) Site + +``` bash +# WORKAROUND: for now, you still need to create a site (which must match the root node name) +# !! in the future, you would want to import *INTO* a given site (and replace its root node) +./flow site:create neosdemo Neos.Demo Neos.Demo:Document.Homepage + +# the following config points to a Neos 8.0 database (adjust to your needs), created by +# the legacy "./flow site:import Neos.Demo" command. +./flow cr:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' +``` + +#### Importing an existing (Neos >= 9.0) Site from an Export + +``` bash +# import the event stream from the Neos.Demo package +./flow cr:import Packages/Sites/Neos.Demo/Resources/Private/Content +``` + +### Running Neos + +> ``` bash +> ./flow server:run +> ``` diff --git a/Readme.rst b/Readme.rst deleted file mode 100644 index 4f54dd91f31..00000000000 --- a/Readme.rst +++ /dev/null @@ -1,208 +0,0 @@ -|Code Climate| |StyleCI| |Latest Stable Version| |License| |Docs| |Slack| |Forum| |Issues| |Translate| |Twitter| - -.. |Code Climate| image:: https://codeclimate.com/github/neos/neos-development-collection/badges/gpa.svg - :target: https://codeclimate.com/github/neos/neos-development-collection -.. |StyleCI| image:: https://styleci.io/repos/40964014/shield?style=flat - :target: https://styleci.io/repos/40964014 -.. |Latest Stable Version| image:: https://poser.pugx.org/neos/neos-development-collection/v/9.0 - :target: https://packagist.org/packages/neos/neos-development-collection -.. |License| image:: https://poser.pugx.org/neos/neos-development-collection/license - :target: https://raw.githubusercontent.com/neos/neos-development-collection/9.0/LICENSE -.. |Docs| image:: https://img.shields.io/badge/documentation-master-blue.svg - :target: https://neos.readthedocs.org/en/9.0/ - :alt: Documentation -.. |Slack| image:: http://slack.neos.io/badge.svg - :target: http://slack.neos.io - :alt: Slack -.. |Forum| image:: https://img.shields.io/badge/forum-Discourse-39c6ff.svg - :target: https://discuss.neos.io/ - :alt: Discussion Forum -.. |Issues| image:: https://img.shields.io/github/issues/neos/neos-development-collection.svg - :target: https://github.com/neos/neos-development-collection/issues - :alt: Issues -.. |Translate| image:: https://img.shields.io/badge/translate-weblate-85ae52.svg - :target: https://hosted.weblate.org/projects/neos/ - :alt: Translation -.. |Twitter| image:: https://img.shields.io/twitter/follow/neoscms.svg?style=social - :target: https://twitter.com/NeosCMS - :alt: Twitter - ---------------------------- -Neos development collection ---------------------------- - -**FOR DOCS ON THE EVENT SOURCED CONTENT REPOSITORY, READ ON BELOW** - -This repository is a collection of packages for the Neos content application platform (learn more on https://www.neos.io/). -The repository is used for development and all pull requests should go into it. - -If you want to install Neos, please have a look at the documentation: https://neos.readthedocs.org/en/latest/ - -Contributing -============ - -If you want to contribute to Neos and want to set up a development environment, then follow these steps: - -``composer create-project neos/neos-development-distribution neos-development 8.3.x-dev --keep-vcs`` - -Note the **-distribution** repository you create a project from, instead of just checking out this repository. - -If you need a different branch, you can either use it from the start (replace the ``8.3.x-dev`` by ``9.0.x-dev`` or whatever you need), or switch after checkout (just make sure to run composer update afterwards to get matching dependencies installed.) In a nutshell, to switch the branch you intend to work on, run: - -``git checkout 9.0 && composer update`` - -The code of the CMS can then be found inside ``Packages/Neos``, which itself is the neos-development-collection Git repository. You commit changes and create pull requests from this repository. - -- To commit changes to Neos switch into the ``Neos`` directory (``cd Packages/Neos``) and do all Git-related work (``git add .``, ``git commit``, etc) there. -- If you want to contribute to the Neos UI, please take a look at the explanations at https://github.com/neos/neos-ui#contributing on how to work with that. -- If you want to contribute to the Flow Framework, you find that inside the ``Packages/Framework`` folder. See https://github.com/neos/flow-development-collection - -In the root directory of the development distribution, you can do the following things: - -To run tests, run ``./bin/phpunit -c ./Build/BuildEssentials/PhpUnit/UnitTests.xml`` for unit tests or ``./bin/phpunit -c ./Build/BuildEssentials/PhpUnit/FunctionalTests.xml`` for functional/integration tests. - -.. note:: We use an upmerging strategy: create all bugfixes to the lowest maintained branch that contains the issue. Typically, this is the second last LTS release - see the diagram at https://www.neos.io/features/release-process.html. - - For new features, pull requests should be made against the branch for the next minor version (named like ``x.y``). Breaking changes must only go into the branch for the next major version. - -For more detailed information, see https://discuss.neos.io/t/development-setup/504, -https://discuss.neos.io/t/creating-a-pull-request/506 and -https://discuss.neos.io/t/git-branch-handling-in-the-neos-project/6013 - - ----------------------------------------------- -New (Event Sourced) Content Repository (ES CR) ----------------------------------------------- - -Prerequisites -============= - -- You need PHP 8.2 installed. -- You need a recent MySQL/MariaDB (PostgreSQL ist not yet supported) -- Please be sure to run off the Neos-Development-Distribution in Branch 9.0, to avoid dependency issues (as described above). - -Setup -===== - -The ES CR has a Docker-compose file included in `Neos.ContentRepository.BehavioralTests` which starts both -Mariadb and Postgres in compatible versions. Additionally, there exists a helper to change the configuration -in your distribution (`/Configuration`) to the correct values matching this database. - -Do the following for setting up everything: - -1. Start the databases: - - .. code-block:: bash - - pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose up -d; popd - - # to stop the databases: - pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose down; popd - # to stop the databases AND remove the stored data: - pushd Packages/Neos/Neos.ContentRepository.BehavioralTests; docker compose down -v; popd - -2. Copy matching Configuration: - - .. code-block:: bash - - cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionConfigurationTemplate/* Configuration/ - -3. Run Doctrine Migrations: - - .. code-block:: bash - - ./flow doctrine:migrate - FLOW_CONTEXT=Testing/Postgres ./flow doctrine:migrate - -4. Setup the Content Repository - - .. code-block:: bash - - ./flow cr:setup - -5. Set up Behat - - .. code-block:: bash - - cp -R Packages/Neos/Neos.ContentRepository.BehavioralTests/DistributionConfigurationTemplate/ Build/Behat - pushd Build/Behat/ - rm composer.lock - composer install - popd - -Site Setup -========== - -You can chose from one of the following options: - -Creating a new Site -------------------- - -.. code-block:: bash - - ./flow site:create neosdemo Neos.Demo Neos.Demo:Document.Homepage - - -Migrating an existing (Neos < 9.0) Site ---------------------------------------- - -.. code-block:: bash - - # WORKAROUND: for now, you still need to create a site (which must match the root node name) - # !! in the future, you would want to import *INTO* a given site (and replace its root node) - ./flow site:create neosdemo Neos.Demo Neos.Demo:Document.Homepage - - # the following config points to a Neos 8.0 database (adjust to your needs), created by - # the legacy "./flow site:import Neos.Demo" command. - ./flow cr:migrateLegacyData --config '{"dbal": {"dbname": "neos80"}, "resourcesPath": "/path/to/neos-8.0/Data/Persistent/Resources"}' - - # TODO: this JSON config is hard to write - we should change this soonish. - - - -Importing an existing (Neos >= 9.0) Site from an Export -------------------------------------------------------- - -.. code-block:: bash - - # import the event stream from the Neos.Demo package - ./flow cr:import Packages/Sites/Neos.Demo/Resources/Private/Content - - -Running Neos -============ - - .. code-block:: bash - - ./flow server:run - - -Running the Tests -================= - -The normal mode is running PHP locally, but running Mariadb / Postgres in containers (so we know -we use the right versions etc). - - .. code-block:: bash - - cd Packages/Neos - composer test:behavioral - -Alternatively, if you want to reproduce errors as they happen inside the CI system, but you -develop on Mac OS, you might want to run the Behat tests in a Docker container (= a linux environment) -as well. We have seen cases where they behave differently, i.e. where they run without race -conditions on OSX, but with race conditions in Linux/Docker. Additionally, the Linux/Docker setup -described below also makes it easy to run the race-condition-detector: - - .. code-block:: bash - - docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml build - docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml up -d - docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml run neos /bin/bash - - # the following commands now run INSIDE the Neos docker container: - FLOW_CONTEXT=Testing/Behat ../../../../../flow raceConditionTracker:reset - - ../../../../../bin/behat -c behat.yml.dist - - FLOW_CONTEXT=Testing/Behat ../../../../../flow raceConditionTracker:analyzeTrace From ee25eaee42bb56d045f1b6d585c1f22badf2dced Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 12 Jun 2024 19:57:15 +0200 Subject: [PATCH 16/26] TASK: Introduce php-stan level 8 to `Neos.SiteKickstarter` --- .../Command/KickstartCommandController.php | 3 +- .../Generator/AfxTemplateGenerator.php | 54 +++++++++---------- .../SitePackageGeneratorInterface.php | 2 +- .../FusionRecursiveDirectoryRenderer.php | 10 ++-- .../Service/SimpleTemplateRenderer.php | 5 +- .../SiteGeneratorCollectingService.php | 3 ++ .../SitePackageGeneratorNameService.php | 5 +- phpstan.neon.dist | 1 + 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php b/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php index 1e9dabe5cd2..34113608867 100755 --- a/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php +++ b/Neos.SiteKickstarter/Classes/Command/KickstartCommandController.php @@ -49,7 +49,7 @@ class KickstartCommandController extends CommandController * @param string $packageKey The packageKey for your site * @param string $siteName The siteName of your site */ - public function siteCommand($packageKey, $siteName) + public function siteCommand($packageKey, $siteName): void { if (!$this->packageManager->isPackageKeyValid($packageKey)) { $this->outputLine('Package key "%s" is not valid. Only UpperCamelCase in the format "Vendor.PackageKey", please!', [$packageKey]); @@ -72,6 +72,7 @@ public function siteCommand($packageKey, $siteName) $nameToClassMap[$name] = $generatorClass; } + /** @var string $generatorName */ $generatorName = $this->output->select( sprintf('What generator do you want to use? (%s): ', array_key_first($selection)), $selection, diff --git a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php index 78551df7222..fc64a3f70b5 100755 --- a/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php +++ b/Neos.SiteKickstarter/Classes/Generator/AfxTemplateGenerator.php @@ -14,6 +14,7 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Flow\Package\FlowPackageInterface; use Neos\Flow\Package\PackageManager; use Neos\Kickstarter\Service\GeneratorService; use Neos\SiteKickstarter\Service\FusionRecursiveDirectoryRenderer; @@ -43,7 +44,7 @@ class AfxTemplateGenerator extends GeneratorService implements SitePackageGenera * * @param string $packageKey * @param string $siteName - * @return array + * @return list * @throws \Neos\Flow\Composer\Exception\InvalidConfigurationException * @throws \Neos\Flow\Package\Exception * @throws \Neos\Flow\Package\Exception\CorruptPackageException @@ -55,56 +56,57 @@ class AfxTemplateGenerator extends GeneratorService implements SitePackageGenera */ public function generateSitePackage(string $packageKey, string $siteName) : array { - $this->packageManager->createPackage($packageKey, [ + $package = $this->packageManager->createPackage($packageKey, [ 'type' => 'neos-site', - "require" => [ - "neos/neos" => "*" + 'require' => [ + 'neos/neos' => '*' ] ]); - $this->generateSitesFusionDirectory($packageKey, $siteName); - $this->generateNodeTypesConfiguration($packageKey); - $this->generateAdditionalFolders($packageKey); + if (!$package instanceof FlowPackageInterface) { + throw new \RuntimeException('Expected to generate flow site package for "' . $packageKey . '" but got ' . get_class($package)); + } + + $this->generateSitesFusionDirectory($package, $siteName); + $this->generateNodeTypesConfiguration($package); + $this->generateAdditionalFolders($package); return $this->generatedFiles; } /** * Render the whole directory of the fusion part - * - * @param $packageKey - * @param $siteName - * @throws \Neos\Flow\Package\Exception\UnknownPackageException */ - protected function generateSitesFusionDirectory(string $packageKey, string $siteName) : void + protected function generateSitesFusionDirectory(FlowPackageInterface $package, string $siteName) : void { + $packageKey = $package->getPackageKey(); $contextVariables = []; $contextVariables['packageKey'] = $packageKey; $contextVariables['siteName'] = $siteName; - $packageKeyDomainPart = substr(strrchr($packageKey, '.'), 1) ?: $packageKey; + $packageKeyDomainPart = $packageKey; + if ($lastPackagePartWithDot = strrchr($packageKey, '.')) { + $packageKeyDomainPart = substr($lastPackagePartWithDot, 1) ?: $packageKey; + } $contextVariables['siteNodeName'] = $packageKeyDomainPart; $fusionRecursiveDirectoryRenderer = new FusionRecursiveDirectoryRenderer(); $fusionRecursiveDirectoryRenderer->renderDirectory( $this->getTemplateFolder() . 'Fusion', - $this->packageManager->getPackage($packageKey)->getResourcesPath() . 'Private/Fusion', + $package->getResourcesPath() . 'Private/Fusion', $contextVariables ); } /** * Generate a example NodeTypes.yaml - * - * @param string $packageKey - * @throws \Neos\Flow\Package\Exception\UnknownPackageException */ - protected function generateNodeTypesConfiguration(string $packageKey) : void + protected function generateNodeTypesConfiguration(FlowPackageInterface $package) : void { $templateFolder = $this->getTemplateFolder() . 'NodeTypes'; - $targetFolder = $this->packageManager->getPackage($packageKey)->getPackagePath() . 'NodeTypes'; + $targetFolder = $package->getPackagePath() . 'NodeTypes'; $contextVariables = [ - 'packageKey' => $packageKey + 'packageKey' => $package->getPackageKey(), ]; foreach (Files::readDirectoryRecursively($templateFolder, '.yaml') as $templatePathAndFilename) { @@ -116,14 +118,10 @@ protected function generateNodeTypesConfiguration(string $packageKey) : void /** * Generate additional folders for site packages. - * - * @param string $packageKey - * @throws \Neos\Flow\Package\Exception\UnknownPackageException - * @throws \Neos\Utility\Exception\FilesException */ - protected function generateAdditionalFolders(string $packageKey) : void + protected function generateAdditionalFolders(FlowPackageInterface $package) : void { - $resourcesPath = $this->packageManager->getPackage($packageKey)->getResourcesPath(); + $resourcesPath = $package->getResourcesPath(); $publicResourcesPath = Files::concatenatePaths([$resourcesPath, 'Public']); foreach (['Images', 'JavaScript', 'Styles'] as $publicResourceFolder) { @@ -133,7 +131,9 @@ protected function generateAdditionalFolders(string $packageKey) : void protected function getTemplateFolder(): string { - return $this->packageManager->getPackage('Neos.SiteKickstarter')->getResourcesPath() . 'Private/AfxGenerator/'; + $currentPackage = $this->packageManager->getPackage('Neos.SiteKickstarter'); + assert($currentPackage instanceof FlowPackageInterface); + return $currentPackage->getResourcesPath() . 'Private/AfxGenerator/'; } public function getGeneratorName(): string diff --git a/Neos.SiteKickstarter/Classes/Generator/SitePackageGeneratorInterface.php b/Neos.SiteKickstarter/Classes/Generator/SitePackageGeneratorInterface.php index b833a6ab5ef..f74c5f73f36 100755 --- a/Neos.SiteKickstarter/Classes/Generator/SitePackageGeneratorInterface.php +++ b/Neos.SiteKickstarter/Classes/Generator/SitePackageGeneratorInterface.php @@ -20,7 +20,7 @@ interface SitePackageGeneratorInterface * * @param string $packageKey * @param string $siteName - * @return array + * @return list */ public function generateSitePackage(string $packageKey, string $siteName): array; diff --git a/Neos.SiteKickstarter/Classes/Service/FusionRecursiveDirectoryRenderer.php b/Neos.SiteKickstarter/Classes/Service/FusionRecursiveDirectoryRenderer.php index d93c5eb8109..01a1e59a025 100755 --- a/Neos.SiteKickstarter/Classes/Service/FusionRecursiveDirectoryRenderer.php +++ b/Neos.SiteKickstarter/Classes/Service/FusionRecursiveDirectoryRenderer.php @@ -28,16 +28,18 @@ class FusionRecursiveDirectoryRenderer * * @param string $srcDirectory * @param string $targetDirectory - * @param array $variables + * @param array $variables * @throws \Neos\Utility\Exception\FilesException */ - public function renderDirectory(string $srcDirectory, string $targetDirectory, array $variables) + public function renderDirectory(string $srcDirectory, string $targetDirectory, array $variables): void { - $files = scandir($srcDirectory); + $files = scandir($srcDirectory) ?: []; foreach ($files as $key => $value) { $path = realpath($srcDirectory . DIRECTORY_SEPARATOR . $value); - + if ($path === false) { + throw new \RuntimeException('Could not read directory "' . $srcDirectory . DIRECTORY_SEPARATOR . $value . '".'); + } $targetPath = $targetDirectory . DIRECTORY_SEPARATOR . $value; if (!is_dir($path)) { diff --git a/Neos.SiteKickstarter/Classes/Service/SimpleTemplateRenderer.php b/Neos.SiteKickstarter/Classes/Service/SimpleTemplateRenderer.php index 168dd82bdf8..089f7b9a4cc 100755 --- a/Neos.SiteKickstarter/Classes/Service/SimpleTemplateRenderer.php +++ b/Neos.SiteKickstarter/Classes/Service/SimpleTemplateRenderer.php @@ -20,12 +20,15 @@ class SimpleTemplateRenderer * contextVariables array * * @param string $templatePathAndFilename - * @param array $contextVariables + * @param array $contextVariables * @return string */ public function render(string $templatePathAndFilename, array $contextVariables) : string { $content = file_get_contents($templatePathAndFilename); + if ($content === false) { + throw new \RuntimeException(sprintf('Could not read template file "%s".', $templatePathAndFilename)); + } foreach ($contextVariables as $key => $value) { $content = str_replace('{' . $key . '}', $value, $content); } diff --git a/Neos.SiteKickstarter/Classes/Service/SiteGeneratorCollectingService.php b/Neos.SiteKickstarter/Classes/Service/SiteGeneratorCollectingService.php index 378a546d6d0..96c95af8718 100644 --- a/Neos.SiteKickstarter/Classes/Service/SiteGeneratorCollectingService.php +++ b/Neos.SiteKickstarter/Classes/Service/SiteGeneratorCollectingService.php @@ -25,6 +25,9 @@ class SiteGeneratorCollectingService */ protected $reflectionService; + /** + * @return list> + */ public function getAllGenerators(): array { return $this->reflectionService->getAllImplementationClassNamesForInterface(SitePackageGeneratorInterface::class); diff --git a/Neos.SiteKickstarter/Classes/Service/SitePackageGeneratorNameService.php b/Neos.SiteKickstarter/Classes/Service/SitePackageGeneratorNameService.php index fcadc776686..33f95cdfa36 100755 --- a/Neos.SiteKickstarter/Classes/Service/SitePackageGeneratorNameService.php +++ b/Neos.SiteKickstarter/Classes/Service/SitePackageGeneratorNameService.php @@ -26,13 +26,10 @@ class SitePackageGeneratorNameService protected $objectManager; /** - * @param string $generatorClass fully qualified namespace + * @param class-string $generatorClass fully qualified namespace */ public function getNameOfSitePackageGenerator(string $generatorClass) : string { - /** - * @var $generator SitePackageGeneratorInterface - */ $generator = $this->objectManager->get($generatorClass); return $generator->getGeneratorName(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c157d6569f9..07203fbad85 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -18,6 +18,7 @@ parameters: - Neos.ContentRepositoryRegistry/Classes - Neos.Neos/Classes - Neos.TimeableNodeVisibility/Classes + - Neos.SiteKickstarter/Classes - Neos.NodeTypes.Form/Classes # todo lint whole fusion package - Neos.Fusion/Classes/Core From 8340466b0ab7fe77f5f2c36b3136b69953e1eb7f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 12 Jun 2024 21:49:38 +0200 Subject: [PATCH 17/26] TASK: Remove `createSitePackage` action Triggering the creation of a composer package via `Neos.SiteKickstarter` from the web will be removed See discussion regarding fragile GUI setups and the preferred use of cli instead: https://github.com/neos/neos-development-collection/issues/4243 --- .../Module/Administration/SitesController.php | 86 +------------------ .../Module/Administration/Sites/NewSite.html | 34 -------- .../Private/Translations/en/Modules.xlf | 27 ------ 3 files changed, 3 insertions(+), 144 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 7ee46bbe473..1c047fc9b4d 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -233,13 +233,9 @@ public function updateSiteAction(Site $site, $newSiteNodeName) } /** - * Create a new site form. - * - * @param Site $site Site to create - * @Flow\IgnoreValidation("$site") - * @return void + * Create a new site form */ - public function newSiteAction(Site $site = null) + public function newSiteAction(): void { // This is not 100% correct, but it is as good as we can get it to work right now try { @@ -259,88 +255,12 @@ public function newSiteAction(Site $site = null) $sitePackages = $this->packageManager->getFilteredPackages('available', 'neos-site'); - $generatorServiceIsAvailable = $this->packageManager->isPackageAvailable('Neos.SiteKickstarter'); - $generatorServices = []; - - if ($generatorServiceIsAvailable) { - /** @var SiteGeneratorCollectingService $siteGeneratorCollectingService */ - $siteGeneratorCollectingService = $this->objectManager->get(SiteGeneratorCollectingService::class); - /** @var SitePackageGeneratorNameService $sitePackageGeneratorNameService */ - $sitePackageGeneratorNameService = $this->objectManager->get(SitePackageGeneratorNameService::class); - - $generatorClasses = $siteGeneratorCollectingService->getAllGenerators(); - - foreach ($generatorClasses as $generatorClass) { - $name = $sitePackageGeneratorNameService->getNameOfSitePackageGenerator($generatorClass); - $generatorServices[$generatorClass] = $name; - } - } - $this->view->assignMultiple([ 'sitePackages' => $sitePackages, - 'documentNodeTypes' => $documentNodeTypes, - 'site' => $site, - 'generatorServiceIsAvailable' => $generatorServiceIsAvailable, - 'generatorServices' => $generatorServices + 'documentNodeTypes' => $documentNodeTypes ]); } - /** - * Create a new site-package and directly import it. - * - * @param string $packageKey Package Name to create - * @param string $generatorClass Generator Class to generate the site package - * @param string $siteName Site Name to create - * @Flow\Validate(argumentName="$packageKey", type="\Neos\Neos\Validation\Validator\PackageKeyValidator") - * @return void - */ - public function createSitePackageAction(string $packageKey, string $generatorClass, string $siteName): void - { - if ($this->packageManager->isPackageAvailable('Neos.SiteKickstarter') === false) { - $this->addFlashMessage( - $this->getModuleLabel('sites.missingPackage.body', ['Neos.SiteKickstarter']), - $this->getModuleLabel('sites.missingPackage.title'), - Message::SEVERITY_ERROR, - [], - 1475736232 - ); - $this->redirect('index'); - } - - if ($this->packageManager->isPackageAvailable($packageKey)) { - $this->addFlashMessage( - $this->getModuleLabel('sites.invalidPackageKey.body', [htmlspecialchars($packageKey)]), - $this->getModuleLabel('sites.invalidPackageKey.title'), - Message::SEVERITY_ERROR, - [], - 1412372021 - ); - $this->redirect('index'); - } - // this should never happen, but if somebody posts unexpected data to the form, - // it should stop here and return some readable error message - if ($this->objectManager->has($generatorClass) === false) { - $this->addFlashMessage( - 'The generator class "%s" is not present.', - 'Missing generator class', - Message::SEVERITY_ERROR, - [$generatorClass] - ); - $this->redirect('index'); - } - - /** @var SitePackageGeneratorInterface $generatorService */ - $generatorService = $this->objectManager->get($generatorClass); - $generatorService->generateSitePackage($packageKey, $siteName); - - $this->controllerContext->getFlashMessageContainer()->addMessage(new Message(sprintf( - $this->getModuleLabel('sites.sitePackagesWasCreated.body', [htmlspecialchars($packageKey)]), - '', - null - ))); - $this->forward('importSite', null, null, ['packageKey' => $packageKey]); - } - /** * Import a site from site package. * diff --git a/Neos.Neos/Resources/Private/Templates/Module/Administration/Sites/NewSite.html b/Neos.Neos/Resources/Private/Templates/Module/Administration/Sites/NewSite.html index 27d33f25da9..fc2a4a830c0 100644 --- a/Neos.Neos/Resources/Private/Templates/Module/Administration/Sites/NewSite.html +++ b/Neos.Neos/Resources/Private/Templates/Module/Administration/Sites/NewSite.html @@ -59,40 +59,6 @@

{neos:backend.translate(id: 'sites.new', value: 'New site', source: 'Modules - - -
- {neos:backend.translate(id: 'sites.createPackage', value: 'Create a new site-package', source: 'Modules')} - - -
- -
- - -
-
-
- -
- - -
-
-
- -
- -
-
- -
- -

{neos:backend.translate(id: 'sites.noNeosKickstarterInfo', value: 'The Neos Kickstarter package is not installed, install it to kickstart new sites.', source: 'Modules') -> f:format.raw()}

-
-
-
-