diff --git a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php index 0469f0f33ec..c1d4c809330 100644 --- a/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php +++ b/Neos.Fusion/Classes/Core/FusionSourceCodeCollection.php @@ -52,6 +52,9 @@ public static function tryFromFilePath(string $filePath): self return self::fromFilePath($filePath); } + /** + * @deprecated with Neos 9, remove me :) + */ public static function tryFromPackageRootFusion(string $packageKey): self { $fusionPathAndFilename = sprintf('resource://%s/Private/Fusion/Root.fusion', $packageKey); diff --git a/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php b/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php new file mode 100644 index 00000000000..b167eff1bdf --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/FusionAutoIncludeHandler.php @@ -0,0 +1,15 @@ +fusionConfigurationCache->cacheFusionConfigurationBySite($site, function () use ($site) { $siteResourcesPackageKey = $site->getSiteResourcesPackageKey(); - return $this->fusionParser->parseFromSource( - $this->fusionSourceCodeFactory->createFromNodeTypeDefinitions($site->getConfiguration()->contentRepositoryId) - ->union( - $this->fusionSourceCodeFactory->createFromAutoIncludes() - ) - ->union( - FusionSourceCodeCollection::tryFromPackageRootFusion($siteResourcesPackageKey) - ) + $this->fusionAutoIncludeHandler->loadFusionFromPackage( + $siteResourcesPackageKey, + $this->fusionSourceCodeFactory->createFromNodeTypeDefinitions($site->getConfiguration()->contentRepositoryId) + ->union( + $this->fusionSourceCodeFactory->createFromAutoIncludes() + ) + ) ); }); } diff --git a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php index 948994053cb..e8766ba3e5a 100644 --- a/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php +++ b/Neos.Neos/Classes/Domain/Service/FusionSourceCodeFactory.php @@ -36,6 +36,9 @@ class FusionSourceCodeFactory #[Flow\InjectConfiguration("fusion.autoInclude")] protected array $autoIncludeConfiguration = []; + #[Flow\Inject] + protected FusionAutoIncludeHandler $fusionAutoIncludeHandler; + #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -50,14 +53,15 @@ public function createFromAutoIncludes(): FusionSourceCodeCollection $sourcecode = FusionSourceCodeCollection::empty(); foreach (array_keys($this->packageManager->getAvailablePackages()) as $packageKey) { if (isset($this->autoIncludeConfiguration[$packageKey]) && $this->autoIncludeConfiguration[$packageKey] === true) { - $sourcecode = $sourcecode->union( - FusionSourceCodeCollection::tryFromPackageRootFusion($packageKey) - ); + $sourcecode = $this->fusionAutoIncludeHandler->loadFusionFromPackage($packageKey, $sourcecode); } } return $sourcecode; } + /** + * @deprecated with Neos 9 - YAGNI from the start :) + */ public function createFromSite(Site $site): FusionSourceCodeCollection { return FusionSourceCodeCollection::tryFromPackageRootFusion($site->getSiteResourcesPackageKey()); diff --git a/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php b/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php new file mode 100644 index 00000000000..38365f9ea68 --- /dev/null +++ b/Neos.Neos/Classes/Domain/Service/ResourceFusionAutoIncludeHandler.php @@ -0,0 +1,22 @@ +union( + FusionSourceCodeCollection::tryFromFilePath(sprintf('resource://%s/Private/Fusion/Root.fusion', $packageKey)) + ); + } +} diff --git a/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php new file mode 100644 index 00000000000..94e2dc76cb3 --- /dev/null +++ b/Neos.Neos/Classes/Testing/TestingFusionAutoIncludeHandler.php @@ -0,0 +1,60 @@ + + */ + private array $overriddenIncludes = []; + + public function setIncludeFusionPackage(string $packageKey): void + { + $this->overriddenIncludes[$packageKey] = true; + } + + public function setFusionForPackage(string $packageKey, FusionSourceCodeCollection $packageFusionSource): void + { + $this->overriddenIncludes[$packageKey] = $packageFusionSource; + } + + public function reset(): void + { + $this->overriddenIncludes = []; + } + + /** + * If no override is set via {@see setIncludeFusionPackage} or {@see setFusionForPackage} we load all the fusion via the default implementation + */ + public function loadFusionFromPackage(string $packageKey, FusionSourceCodeCollection $sourceCodeCollection): FusionSourceCodeCollection + { + if ($this->overriddenIncludes === []) { + return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } + $override = $this->overriddenIncludes[$packageKey] ?? null; + if ($override === null) { + return $sourceCodeCollection; + } + if ($override === true) { + return $this->defaultHandler->loadFusionFromPackage($packageKey, $sourceCodeCollection); + } + return $sourceCodeCollection->union($override); + } +} diff --git a/Neos.Neos/Configuration/Objects.yaml b/Neos.Neos/Configuration/Objects.yaml index be5cd60dd91..20da5dd98aa 100644 --- a/Neos.Neos/Configuration/Objects.yaml +++ b/Neos.Neos/Configuration/Objects.yaml @@ -20,6 +20,9 @@ Neos\Neos\Domain\Service\FusionConfigurationCache: 2: setting: "Neos.Neos.fusion.enableObjectTreeCache" +Neos\Neos\Domain\Service\FusionAutoIncludeHandler: + className: Neos\Neos\Domain\Service\ResourceFusionAutoIncludeHandler + Neos\Fusion\Core\Cache\RuntimeContentCache: properties: serializer: diff --git a/Neos.Neos/Configuration/Testing/Objects.yaml b/Neos.Neos/Configuration/Testing/Objects.yaml new file mode 100644 index 00000000000..faa9e83523a --- /dev/null +++ b/Neos.Neos/Configuration/Testing/Objects.yaml @@ -0,0 +1,2 @@ +Neos\Neos\Domain\Service\FusionAutoIncludeHandler: + className: Neos\Neos\Testing\TestingFusionAutoIncludeHandler diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php index 9d06ce491eb..fce5b7a3278 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -39,6 +39,7 @@ class FeatureContext implements BehatContext use CRBehavioralTestsSubjectProvider; use RoutingTrait; use MigrationsTrait; + use FrontendNodeControllerTrait; use FusionTrait; use ContentCacheTrait; diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php new file mode 100644 index 00000000000..982262f17d2 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/FrontendNodeControllerTrait.php @@ -0,0 +1,92 @@ + $className + * + * @return T + */ + abstract private function getObject(string $className): object; + + /** + * @BeforeScenario + */ + public function setupFrontendNodeControllerTrait(): void + { + $this->getObject(ContentCache::class)->flush(); + $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class)->reset(); + $this->frontendNodeControllerResponse = null; + } + + /** + * @When the Fusion code for package :package is: + */ + public function iHaveTheFollowingFusionCodeForTheSite(PyStringNode $fusionCode, string $package) + { + $testingFusionHandler = $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class); + $testingFusionHandler->setFusionForPackage($package, \Neos\Fusion\Core\FusionSourceCodeCollection::fromString($fusionCode->getRaw())); + } + + /** + * @When I dispatch the following request :requestUri + */ + public function iDispatchTheFollowingRequest(string $requestUri) + { + $testingFusionHandler = $this->getObject(\Neos\Neos\Testing\TestingFusionAutoIncludeHandler::class); + $testingFusionHandler->setIncludeFusionPackage('Neos.Fusion'); + $testingFusionHandler->setIncludeFusionPackage('Neos.Neos'); + + $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', $requestUri); + + $this->frontendNodeControllerResponse = $this->getObject(\Neos\Flow\Http\Middleware\MiddlewaresChain::class)->handle( + $httpRequest + ); + } + + /** + * @Then I expect the following response header: + */ + public function iExpectTheFollowingResponseHeader(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->frontendNodeControllerResponse); + Assert::assertSame($expectedResult->getRaw(), $this->frontendNodeControllerResponse->getBody()->getContents()); + } + + /** + * @Then I expect the following response: + */ + public function iExpectTheFollowingResponse(PyStringNode $expectedResult): void + { + Assert::assertNotNull($this->frontendNodeControllerResponse); + Assert::assertEquals($expectedResult->getRaw(), str_replace("\r\n", "\n", Message::toString($this->frontendNodeControllerResponse))); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 3eb2dc1a93a..f5232d55a10 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -82,14 +82,15 @@ abstract private function getObject(string $className): object; /** * @Given A site exists for node name :nodeName * @Given A site exists for node name :nodeName and domain :domain + * @Given A site exists for node name :nodeName and domain :domain and package :package */ - public function theSiteExists(string $nodeName, string $domain = null): void + public function theSiteExists(string $nodeName, string $domain = null, string $package = null): void { $siteRepository = $this->getObject(SiteRepository::class); $persistenceManager = $this->getObject(PersistenceManagerInterface::class); $site = new Site($nodeName); - $site->setSiteResourcesPackageKey('Neos.Neos'); + $site->setSiteResourcesPackageKey($package ?: 'Neos.Neos'); $site->setState(Site::STATE_ONLINE); $siteRepository->add($site); diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature new file mode 100644 index 00000000000..8e2d8dfcd12 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/FrontendNodeController/DefaultFusionRendering.feature @@ -0,0 +1,113 @@ +@flowEntities +Feature: Test the default Fusion rendering for a request + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:ContentCollection': {} + 'Neos.Neos:Content': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + 'Neos.Neos:Test.DocumentType': + superTypes: + 'Neos.Neos:Document': true + childNodes: + main: + type: 'Neos.Neos:ContentCollection' + 'Neos.Neos:Test.ContentType': + superTypes: + 'Neos.Neos:Content': true + properties: + text: + type: string + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | tetheredDescendantNodeAggregateIds | nodeName | + | a | root | Neos.Neos:Site | {"title": "Node a"} | {} | a | + | a1 | a | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a1", "title": "Node a1"} | {"main": "a-tetherton" } | | + | a1a1 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my first text"} | {} | | + | a1a2 | a-tetherton | Neos.Neos:Test.ContentType | {"text": "my second text"} | {} | | + And A site exists for node name "a" and domain "http://localhost" and package "Vendor.Site" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + 'a': + preset: default + uriPathSuffix: '' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + + Scenario: Default output + And the Fusion code for package "Vendor.Site" is: + """fusion + prototype(Neos.Neos:Test.DocumentType) < prototype(Neos.Neos:Page) { + body { + content = Neos.Fusion:Component { + renderer = afx` + {String.chr(10)}title: {node.properties.title} + {String.chr(10)}children: + {String.chr(10)} + ` + } + } + } + prototype(Neos.Neos:Test.ContentType) < prototype(Neos.Neos:ContentComponent) { + text = Neos.Neos:Editable { + property = 'text' + } + + renderer = afx` + [{props.text}] + ` + } + """ + + When I dispatch the following request "/a1" + Then I expect the following response: + """ + HTTP/1.1 200 OK + Content-Type: text/html + X-Flow-Powered: Flow/dev Neos/dev + Content-Length: 486 + + + + Node a1 + title: Node a1 + children:
[my first text][my second text]
+ + """