Skip to content

Commit

Permalink
Merge pull request #4735 from neos/feature/4582-site-config-presets
Browse files Browse the repository at this point in the history
FEATURE: Site configuration presets
  • Loading branch information
bwaidelich authored Apr 30, 2024
2 parents 8aec861 + 1909e8c commit 0b46cbc
Show file tree
Hide file tree
Showing 16 changed files with 151 additions and 53 deletions.
32 changes: 29 additions & 3 deletions Neos.Neos/Classes/Domain/Model/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Doctrine\ORM\Mapping as ORM;
use Neos\Flow\Annotations as Flow;
use Neos\Media\Domain\Model\AssetCollection;
use Neos\Utility\Arrays;

/**
* Domain model of a site
Expand All @@ -35,12 +36,19 @@ class Site
public const STATE_OFFLINE = 2;

/**
* @Flow\InjectConfiguration(path="sites")
* @var array
* @phpstan-var array<string,array<string,mixed>>
*/
#[Flow\InjectConfiguration(path: 'sites')]
protected $sitesConfiguration = [];

/**
* @var array
* @phpstan-var array<string,mixed>
*/
#[Flow\InjectConfiguration(path: 'sitePresets')]
protected $sitePresetsConfiguration = [];

/**
* Name of the site
*
Expand Down Expand Up @@ -372,7 +380,25 @@ public function emitSiteChanged()

public function getConfiguration(): SiteConfiguration
{
// we DO NOT want recursive merge here
return SiteConfiguration::fromArray($this->sitesConfiguration[$this->nodeName] ?? $this->sitesConfiguration['*']);
if (array_key_exists($this->nodeName, $this->sitesConfiguration)) {
$siteSettingsPath = $this->nodeName;
} else {
if (!array_key_exists('*', $this->sitesConfiguration)) {
throw new \RuntimeException(sprintf('Missing configuration for "Neos.Neos.sites.%s" or fallback "Neos.Neos.sites.*"', $this->nodeName), 1714230658);
}
$siteSettingsPath = '*';
}
$siteSettings = $this->sitesConfiguration[$siteSettingsPath];
if (isset($siteSettings['preset'])) {
if (!is_string($siteSettings['preset'])) {
throw new \RuntimeException(sprintf('Invalid "preset" configuration for "Neos.Neos.sites.%s". Expected string, got: %s', $siteSettingsPath, get_debug_type($siteSettings['preset'])), 1699785648);
}
if (!isset($this->sitePresetsConfiguration[$siteSettings['preset']]) || !is_array($this->sitePresetsConfiguration[$siteSettings['preset']])) {
throw new \RuntimeException(sprintf('Site settings "Neos.Neos.sites.%s" refer to a preset "%s", but no corresponding preset is configured', $siteSettingsPath, $siteSettings['preset']), 1699785736);
}
$siteSettings = Arrays::arrayMergeRecursiveOverrule($this->sitePresetsConfiguration[$siteSettings['preset']], $siteSettings);
unset($siteSettings['preset']);
}
return SiteConfiguration::fromArray($siteSettings);
}
}
18 changes: 18 additions & 0 deletions Neos.Neos/Configuration/Settings.Sites.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# #
# Site specific settings #
# #
# This file contains settings specific to Neos Sites #

Neos:

Neos:
sitePresets:
'default':
uriPathSuffix: '.html'
contentRepository: default
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory
sites:
'*':
preset: 'default'
16 changes: 0 additions & 16 deletions Neos.Neos/Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
Neos:

Neos:
contentDimensions:
resolution:
uriPathSegmentDelimiter: '_'

fusion:
# if set to true, Fusion is cached on a per-site basis.
Expand Down Expand Up @@ -54,19 +51,6 @@ Neos:
More information and contribution opportunities at https://www.neos.io
-->
routing:
# Setting this to true allows to use an empty uriSegment for default dimensions.
# The only limitation is that all segments must be unique across all dimenions.
supportEmptySegmentForDimensions: true

sites:
'*':
uriPathSuffix: '.html'
contentRepository: default
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory

nodeTypes:
groups:
general:
Expand Down
4 changes: 2 additions & 2 deletions Neos.Neos/Migrations/Code/Version20230801154834.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


/**
* Replace defaultUriSuffix configuration with uriPathSuffix in default site configuration
* Replace defaultUriSuffix configuration with uriPathSuffix in default site preset configuration
*/
class Version20230801154834 extends AbstractMigration
{
Expand All @@ -16,6 +16,6 @@ public function getIdentifier(): string

public function up(): void
{
$this->moveSettingsPaths(['Neos', 'Flow', 'mvc', 'routes', 'Neos.Neos', 'variables', 'defaultUriSuffix'], ['Neos', 'Neos', 'sites', '*', 'uriPathSuffix']);
$this->moveSettingsPaths(['Neos', 'Flow', 'mvc', 'routes', 'Neos.Neos', 'variables', 'defaultUriSuffix'], ['Neos', 'Neos', 'sitePresets', 'default', 'uriPathSuffix']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Feature: Basic routing functionality (match & resolve document nodes in one dime
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: 'default'
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
defaultDimensionSpacePoint:
market: DE
Expand Down Expand Up @@ -111,8 +112,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
defaultDimensionSpacePoint:
market: DE
Expand Down Expand Up @@ -170,8 +172,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory
Expand Down Expand Up @@ -239,8 +242,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory
Expand Down Expand Up @@ -306,8 +310,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory
Expand Down Expand Up @@ -367,8 +372,9 @@ Feature: Routing functionality with multiple content dimensions
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Feature: Routing behavior of removed, disabled and re-enabled nodes
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,15 @@ Feature: Linking between multiple websites
Neos:
Neos:
sites:
'*':
contentRepository: default
'site-1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
'site-2':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ Feature: Route cache invalidation
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ Feature: Routing behavior of shortcut nodes
Neos:
Neos:
sites:
'*':
contentRepository: default
'node1':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ Feature: Tests for site node child documents. These are special in that they hav
Neos:
Neos:
sites:
'*':
contentRepository: default
'site':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
5 changes: 3 additions & 2 deletions Neos.Neos/Tests/Behavior/Features/Fusion/ContentCase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ Feature: Tests for the "Neos.Neos:ContentCase" Fusion prototype
Neos:
Neos:
sites:
'*':
contentRepository: default
'a':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ Feature: Tests for the "Neos.Neos:ContentCollection" Fusion prototype
Neos:
Neos:
sites:
'*':
contentRepository: default
'a':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
5 changes: 3 additions & 2 deletions Neos.Neos/Tests/Behavior/Features/Fusion/ConvertUris.feature
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ Feature: Tests for the "Neos.Neos:ConvertUris" Fusion prototype
Neos:
Neos:
sites:
'*':
contentRepository: default
'a':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
5 changes: 3 additions & 2 deletions Neos.Neos/Tests/Behavior/Features/Fusion/Menu.feature
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ Feature: Tests for the "Neos.Neos:Menu" and related Fusion prototypes
Neos:
Neos:
sites:
'*':
contentRepository: default
'a':
preset: default
uriPathSuffix: ''
contentDimensions:
resolver:
factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory
Expand Down
48 changes: 48 additions & 0 deletions Neos.Neos/Tests/Unit/Domain/Model/SiteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
use Neos\Flow\Tests\UnitTestCase;
use Neos\Neos\Domain\Model\Site;
use Neos\Utility\ObjectAccess;

/**
* Testcase for the "Site" domain model
Expand Down Expand Up @@ -57,4 +58,51 @@ public function theSiteResourcesPackageKeyCanBeSetAndRetrieved()
$site->setSiteResourcesPackageKey('Foo');
self::assertSame('Foo', $site->getSiteResourcesPackageKey());
}

public static function getConfigurationFailingDataProvider(): iterable
{
yield 'no matching nor default site config' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => [], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Missing configuration for "Neos.Neos.sites.siteNodeName" or fallback "Neos.Neos.sites.*"'];
yield 'referring non-string preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => false]], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Invalid "preset" configuration for "Neos.Neos.sites.siteNodeName". Expected string, got: bool'];
yield 'referring non-existing preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => 'nonExistingPreset']], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'Site settings "Neos.Neos.sites.siteNodeName" refer to a preset "nonExistingPreset"'];
yield 'missing content repository identifier' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => []], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'There is no content repository identifier configured in Sites configuration in Settings.yaml: Neos.Neos.sites.*.contentRepository'];
yield 'missing content dimension resolver factory' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'default']], 'sitePresetConfiguration' => [], 'expectedExceptionMessage' => 'No Dimension Resolver Factory configured at Neos.Neos.sites.*.contentDimensions.resolver.factoryClassName'];
}

/**
* @test
* @dataProvider getConfigurationFailingDataProvider
*/
public function getConfigurationFailingTests(string $nodeTypeName, array $sitesConfiguration, array $sitePresetsConfiguration, string $expectedExceptionMessage): void
{
$site = new Site($nodeTypeName);
ObjectAccess::setProperty($site, 'sitesConfiguration', $sitesConfiguration, true);
ObjectAccess::setProperty($site, 'sitePresetsConfiguration', $sitePresetsConfiguration, true);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$site->getConfiguration();
}

public static function getConfigurationSucceedingDataProvider(): iterable
{
yield 'minimal configuration' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'default', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Foo']]]], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'default', 'contentDimensionResolverFactoryClassName' => 'Foo', 'contentDimensionResolverOptions' => [], 'defaultDimensionSpacePoint' => [], 'uriPathSuffix' => '']];
yield 'full configuration' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-suffix']], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de'], 'uriPathSuffix' => 'some-suffix']];
yield 'full configuration from fallback' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['*' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-suffix']], 'sitePresetConfiguration' => [], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de'], 'uriPathSuffix' => 'some-suffix']];
yield 'full configuration merged with preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['siteNodeName' => ['preset' => 'somePreset', 'contentDimensions' => ['defaultDimensionSpacePoint' => ['country' => 'DE']], 'uriPathSuffix' => 'some-overridden-suffix']], 'sitePresetConfiguration' => ['somePreset' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-default-suffix']], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de', 'country' => 'DE'], 'uriPathSuffix' => 'some-overridden-suffix']];
yield 'full configuration from fallback merged with preset' => ['nodeTypeName' => 'siteNodeName', 'sitesConfiguration' => ['*' => ['preset' => 'somePreset', 'contentDimensions' => ['defaultDimensionSpacePoint' => ['country' => 'DE']], 'uriPathSuffix' => 'some-overridden-suffix']], 'sitePresetConfiguration' => ['somePreset' => ['contentRepository' => 'custom_repo', 'contentDimensions' => ['resolver' => ['factoryClassName' => 'Bar', 'options' => ['some' => 'options']], 'defaultDimensionSpacePoint' => ['language' => 'de']], 'uriPathSuffix' => 'some-default-suffix']], 'expectedConfiguration' => ['contentRepositoryId' => 'custom_repo', 'contentDimensionResolverFactoryClassName' => 'Bar', 'contentDimensionResolverOptions' => ['some' => 'options'], 'defaultDimensionSpacePoint' => ['language' => 'de', 'country' => 'DE'], 'uriPathSuffix' => 'some-overridden-suffix']];
}

/**
* @test
* @dataProvider getConfigurationSucceedingDataProvider
*/
public function getConfigurationSucceedingTests(string $nodeTypeName, array $sitesConfiguration, array $sitePresetsConfiguration, array $expectedConfiguration): void
{
$site = new Site($nodeTypeName);
ObjectAccess::setProperty($site, 'sitesConfiguration', $sitesConfiguration, true);
ObjectAccess::setProperty($site, 'sitePresetsConfiguration', $sitePresetsConfiguration, true);

$configuration = $site->getConfiguration();
self::assertSame($expectedConfiguration, json_decode(json_encode($configuration), true));
}
}

0 comments on commit 0b46cbc

Please sign in to comment.