diff --git a/Content/ContentTypeResolver/SingleSnippetSelectionResolver.php b/Content/ContentTypeResolver/SingleSnippetSelectionResolver.php
new file mode 100644
index 0000000..b7fccb3
--- /dev/null
+++ b/Content/ContentTypeResolver/SingleSnippetSelectionResolver.php
@@ -0,0 +1,115 @@
+contentMapper = $contentMapper;
+ $this->structureResolver = $structureResolver;
+ $this->defaultSnippetManager = $defaultSnippetManager;
+ }
+
+ public static function getContentType(): string
+ {
+ return 'single_snippet_selection';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve($data, PropertyInterface $property, string $locale, array $attributes = []): ContentView
+ {
+ /** @var StructureBridge $structure */
+ $structure = $property->getStructure();
+ $webspaceKey = $structure->getWebspaceKey();
+ $shadowLocale = $structure->getIsShadow() ? $structure->getShadowBaseLanguage() : null;
+
+ $params = $property->getParams();
+ /** @var bool $loadExcerpt */
+ $loadExcerpt = isset($params['loadExcerpt']) ? $params['loadExcerpt']->getValue() : false;
+ /** @var string $defaultArea */
+ $defaultArea = isset($params['default']) ? $params['default']->getValue() : null;
+
+ $snippetId = $data ?? null;
+ if (empty($snippetId) && $defaultArea) {
+ $defaultSnippetId = $this->getDefaultSnippetId($webspaceKey, $defaultArea, $locale);
+ $snippetId = $defaultSnippetId ?: null;
+ }
+
+ if (empty($snippetId)) {
+ return new ContentView(null);
+ }
+
+ /** @var SnippetBridge $snippet */
+ $snippet = $this->contentMapper->load($snippetId, $webspaceKey, $locale);
+
+ if (!$snippet->getHasTranslation() && null !== $shadowLocale) {
+ /** @var SnippetBridge $snippet */
+ $snippet = $this->contentMapper->load($snippetId, $webspaceKey, $shadowLocale);
+ }
+
+ $snippet->setIsShadow(null !== $shadowLocale);
+ $snippet->setShadowBaseLanguage($shadowLocale);
+
+ $resolvedSnippet = $this->structureResolver->resolve($snippet, $locale, $loadExcerpt);
+
+ return new ContentView($resolvedSnippet, ['id' => $data]);
+ }
+
+ private function getDefaultSnippetId(string $webspaceKey, string $snippetArea, string $locale): ?string
+ {
+ try {
+ /** @var SnippetDocument|null $snippet */
+ $snippet = $this->defaultSnippetManager->load($webspaceKey, $snippetArea, $locale);
+ } catch (WrongSnippetTypeException $exception) {
+ return null;
+ }
+
+ if (!$snippet) {
+ return null;
+ }
+
+ return $snippet->getUuid();
+ }
+}
diff --git a/Resources/config/content-type-resolvers.xml b/Resources/config/content-type-resolvers.xml
index 41d4db1..1a26cc7 100644
--- a/Resources/config/content-type-resolvers.xml
+++ b/Resources/config/content-type-resolvers.xml
@@ -157,5 +157,17 @@
+
+
+
+
+
+
+
+
diff --git a/Tests/Unit/Content/ContentTypeResolver/SingleSnippetSelectionResolverTest.php b/Tests/Unit/Content/ContentTypeResolver/SingleSnippetSelectionResolverTest.php
new file mode 100644
index 0000000..131808f
--- /dev/null
+++ b/Tests/Unit/Content/ContentTypeResolver/SingleSnippetSelectionResolverTest.php
@@ -0,0 +1,268 @@
+contentMapper = $this->prophesize(ContentMapperInterface::class);
+ $this->structureResolver = $this->prophesize(StructureResolverInterface::class);
+ $this->defaultSnippetManager = $this->prophesize(DefaultSnippetManagerInterface::class);
+
+ $this->snippetSelectionResolver = new SingleSnippetSelectionResolver(
+ $this->contentMapper->reveal(),
+ $this->structureResolver->reveal(),
+ $this->defaultSnippetManager->reveal()
+ );
+ }
+
+ public function testGetContentType(): void
+ {
+ self::assertSame('single_snippet_selection', $this->snippetSelectionResolver::getContentType());
+ }
+
+ public function testResolveWithExcerpt(): void
+ {
+ $structure = $this->prophesize(StructureBridge::class);
+ $structure->getWebspaceKey()->willReturn('webspace-key');
+ $structure->getIsShadow()->willReturn(false);
+
+ $property = $this->prophesize(PropertyInterface::class);
+ $property->getStructure()->willReturn($structure->reveal());
+ $property->getParams()->willReturn([
+ 'loadExcerpt' => new PropertyParameter('loadExcerpt', true),
+ ]);
+
+ $snippet1 = $this->prophesize(SnippetBridge::class);
+ $snippet1->getHasTranslation()->willReturn(true);
+ $snippet1->setIsShadow(false)->shouldBeCalled();
+ $snippet1->setShadowBaseLanguage(null)->shouldBeCalled();
+
+ $this->contentMapper->load('snippet-1', 'webspace-key', 'en')->willReturn($snippet1->reveal());
+ $this->structureResolver->resolve($snippet1->reveal(), 'en', true)->willReturn([
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ]);
+
+ $result = $this->snippetSelectionResolver->resolve('snippet-1', $property->reveal(), 'en', []);
+ $this->assertInstanceOf(ContentView::class, $result);
+ $this->assertSame(
+ [
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ],
+ $result->getContent()
+ );
+
+ $this->assertSame(
+ ['id' => 'snippet-1'],
+ $result->getView()
+ );
+ }
+
+ public function testResolveShadowLocale(): void
+ {
+ $structure = $this->prophesize(StructureBridge::class);
+ $structure->getWebspaceKey()->willReturn('webspace-key');
+ $structure->getIsShadow()->willReturn(true);
+ $structure->getShadowBaseLanguage()->willReturn('de');
+
+ $property = $this->prophesize(PropertyInterface::class);
+ $property->getStructure()->willReturn($structure->reveal());
+ $property->getParams()->willReturn([]);
+
+ $snippet1 = $this->prophesize(SnippetBridge::class);
+ $snippet1->getHasTranslation()->willReturn(true);
+ $snippet1->setIsShadow(true)->shouldBeCalled();
+ $snippet1->setShadowBaseLanguage('de')->shouldBeCalled();
+
+ $this->contentMapper->load('snippet-1', 'webspace-key', 'en')->willReturn($snippet1->reveal());
+ $this->structureResolver->resolve($snippet1->reveal(), 'en', false)->willReturn([
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ]);
+
+ $result = $this->snippetSelectionResolver->resolve('snippet-1', $property->reveal(), 'en', []);
+ $this->assertInstanceOf(ContentView::class, $result);
+ $this->assertSame(
+ [
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ],
+ $result->getContent()
+ );
+
+ $this->assertSame(
+ ['id' => 'snippet-1'],
+ $result->getView()
+ );
+ }
+
+ public function testResolveShadowLocaleNoTranslation(): void
+ {
+ $structure = $this->prophesize(StructureBridge::class);
+ $structure->getWebspaceKey()->willReturn('webspace-key');
+ $structure->getIsShadow()->willReturn(true);
+ $structure->getShadowBaseLanguage()->willReturn('de');
+
+ $property = $this->prophesize(PropertyInterface::class);
+ $property->getStructure()->willReturn($structure->reveal());
+ $property->getParams()->willReturn([]);
+
+ $snippet1en = $this->prophesize(SnippetBridge::class);
+ $snippet1en->getHasTranslation()->willReturn(false);
+
+ $snippet1de = $this->prophesize(SnippetBridge::class);
+ $snippet1de->setIsShadow(true)->shouldBeCalled();
+ $snippet1de->setShadowBaseLanguage('de')->shouldBeCalled();
+
+ $this->contentMapper->load('snippet-1', 'webspace-key', 'en')->willReturn($snippet1en->reveal());
+ $this->contentMapper->load('snippet-1', 'webspace-key', 'de')->willReturn($snippet1de->reveal());
+ $this->structureResolver->resolve($snippet1de->reveal(), 'en', false)->willReturn([
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ]);
+
+ $result = $this->snippetSelectionResolver->resolve('snippet-1', $property->reveal(), 'en', []);
+ $this->assertInstanceOf(ContentView::class, $result);
+ $this->assertSame(
+ [
+ 'id' => 'snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ],
+ $result->getContent()
+ );
+
+ $this->assertSame(
+ ['id' => 'snippet-1'],
+ $result->getView()
+ );
+ }
+
+ public function testResolveDataIsNull(): void
+ {
+ $structure = $this->prophesize(StructureBridge::class);
+ $structure->getWebspaceKey()->willReturn('webspace-key');
+ $structure->getIsShadow()->willReturn(false);
+
+ $property = $this->prophesize(PropertyInterface::class);
+ $property->getStructure()->willReturn($structure->reveal());
+ $property->getParams()->willReturn([]);
+
+ $result = $this->snippetSelectionResolver->resolve(null, $property->reveal(), 'en', []);
+ $this->assertInstanceOf(ContentView::class, $result);
+ $this->assertNull(
+ $result->getContent()
+ );
+
+ $this->assertSame(
+ [],
+ $result->getView()
+ );
+ }
+
+ public function testResolveDataIsNullWithDefaultArea(): void
+ {
+ $structure = $this->prophesize(StructureBridge::class);
+ $structure->getWebspaceKey()->willReturn('webspace-key');
+ $structure->getIsShadow()->willReturn(false);
+
+ $property = $this->prophesize(PropertyInterface::class);
+ $property->getStructure()->willReturn($structure->reveal());
+ $property->getParams()->willReturn([
+ 'default' => new PropertyParameter('default', 'test-snippet-area'),
+ ]);
+
+ $defaultSnippetDocument = $this->prophesize(SnippetDocument::class);
+ $defaultSnippetDocument->getUuid()->willReturn('default-snippet-1');
+ $this->defaultSnippetManager->load('webspace-key', 'test-snippet-area', 'en')
+ ->willReturn($defaultSnippetDocument->reveal());
+
+ $defaultSnippet = $this->prophesize(SnippetBridge::class);
+ $defaultSnippet->getHasTranslation()->willReturn(true);
+ $defaultSnippet->setIsShadow(false)->shouldBeCalled();
+ $defaultSnippet->setShadowBaseLanguage(null)->shouldBeCalled();
+
+ $this->contentMapper->load('default-snippet-1', 'webspace-key', 'en')->willReturn($defaultSnippet->reveal());
+ $this->structureResolver->resolve($defaultSnippet->reveal(), 'en', false)->willReturn([
+ 'id' => 'default-snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ]);
+
+ $result = $this->snippetSelectionResolver->resolve(null, $property->reveal(), 'en', []);
+ $this->assertInstanceOf(ContentView::class, $result);
+ $this->assertSame(
+ [
+ 'id' => 'default-snippet-1',
+ 'template' => 'test',
+ 'content' => [],
+ 'view' => [],
+ ],
+ $result->getContent()
+ );
+
+ $this->assertSame(
+ ['id' => null],
+ $result->getView()
+ );
+ }
+}