diff --git a/src/bundle/Core/DependencyInjection/IbexaCoreExtension.php b/src/bundle/Core/DependencyInjection/IbexaCoreExtension.php index 184db428f4..5473aeff52 100644 --- a/src/bundle/Core/DependencyInjection/IbexaCoreExtension.php +++ b/src/bundle/Core/DependencyInjection/IbexaCoreExtension.php @@ -370,6 +370,7 @@ private function handleApiLoading(ContainerBuilder $container, FileLoader $loade $coreLoader->load('user_preference.yml'); $coreLoader->load('events.yml'); $coreLoader->load('thumbnails.yml'); + $coreLoader->load('content_publication.yaml'); $coreLoader->load('tokens.yml'); $coreLoader->load('content_location_mapper.yml'); diff --git a/src/contracts/Repository/Strategy/ContentPublication/ContentPublicationResult.php b/src/contracts/Repository/Strategy/ContentPublication/ContentPublicationResult.php new file mode 100644 index 0000000000..7db844133f --- /dev/null +++ b/src/contracts/Repository/Strategy/ContentPublication/ContentPublicationResult.php @@ -0,0 +1,22 @@ + $translations List of language codes of translations which will be + * included in a published version + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + public function publishVersion(VersionInfo $versionInfo, array $translations = Language::ALL): ContentPublicationResult; +} diff --git a/src/lib/Repository/Strategy/ContentPublication/ChainContentPublicationStrategy.php b/src/lib/Repository/Strategy/ContentPublication/ChainContentPublicationStrategy.php new file mode 100644 index 0000000000..ce76e9df4b --- /dev/null +++ b/src/lib/Repository/Strategy/ContentPublication/ChainContentPublicationStrategy.php @@ -0,0 +1,58 @@ + $strategies + */ + public function __construct( + private iterable $strategies, + ) { + } + + public function supports(): bool + { + foreach ($this->strategies as $strategy) { + if ($strategy->supports()) { + return true; + } + } + + return false; + } + + public function publishVersion(VersionInfo $versionInfo, array $translations = Language::ALL): ContentPublicationResult + { + foreach ($this->strategies as $strategy) { + if ($strategy->supports()) { + return $strategy->publishVersion($versionInfo, $translations); + } + } + + throw new LogicException(sprintf( + 'No content publication strategy supports the current publication. At least %s' + . ' must be tagged with "ibexa.repository.content.publication_strategy".', + SynchronousContentPublicationStrategy::class, + )); + } +} diff --git a/src/lib/Repository/Strategy/ContentPublication/SynchronousContentPublicationStrategy.php b/src/lib/Repository/Strategy/ContentPublication/SynchronousContentPublicationStrategy.php new file mode 100644 index 0000000000..606812575f --- /dev/null +++ b/src/lib/Repository/Strategy/ContentPublication/SynchronousContentPublicationStrategy.php @@ -0,0 +1,41 @@ +contentService->publishVersion($versionInfo, $translations) + ); + } +} diff --git a/src/lib/Resources/settings/content_publication.yaml b/src/lib/Resources/settings/content_publication.yaml new file mode 100644 index 0000000000..87a0162440 --- /dev/null +++ b/src/lib/Resources/settings/content_publication.yaml @@ -0,0 +1,22 @@ +services: + _defaults: + public: false + autoconfigure: true + autowire: true + + # Content publication strategies. The first strategy (in priority order) whose supports() + # returns true executes; the synchronous strategy is the always-applicable fallback. + # Additional strategies (e.g. asynchronous publication) are provided by dedicated packages + # via the "ibexa.repository.content.publication_strategy" tag with a higher priority. + Ibexa\Core\Repository\Strategy\ContentPublication\SynchronousContentPublicationStrategy: + arguments: + $contentService: '@ibexa.api.service.content' + tags: + - { name: ibexa.repository.content.publication_strategy, priority: -100 } + + Ibexa\Core\Repository\Strategy\ContentPublication\ChainContentPublicationStrategy: + arguments: + $strategies: !tagged_iterator ibexa.repository.content.publication_strategy + + Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationStrategyInterface: + alias: Ibexa\Core\Repository\Strategy\ContentPublication\ChainContentPublicationStrategy diff --git a/tests/lib/Repository/Strategy/Publication/ChainContentPublicationStrategyTest.php b/tests/lib/Repository/Strategy/Publication/ChainContentPublicationStrategyTest.php new file mode 100644 index 0000000000..4fdbe1e1d3 --- /dev/null +++ b/tests/lib/Repository/Strategy/Publication/ChainContentPublicationStrategyTest.php @@ -0,0 +1,106 @@ +createStub(VersionInfo::class); + $result = new ContentPublicationResult(null); + + $notSupportingStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $notSupportingStrategy + ->expects(self::once()) + ->method('supports') + ->willReturn(false); + $notSupportingStrategy + ->expects(self::never()) + ->method('publishVersion'); + + $supportingStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $supportingStrategy + ->expects(self::once()) + ->method('supports') + ->willReturn(true); + $supportingStrategy + ->expects(self::once()) + ->method('publishVersion') + ->with(self::identicalTo($versionInfo), ['eng-GB']) + ->willReturn($result); + + $neverConsultedStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $neverConsultedStrategy + ->expects(self::never()) + ->method('supports'); + $neverConsultedStrategy + ->expects(self::never()) + ->method('publishVersion'); + + $chain = new ChainContentPublicationStrategy([ + $notSupportingStrategy, + $supportingStrategy, + $neverConsultedStrategy, + ]); + + self::assertSame($result, $chain->publishVersion($versionInfo, ['eng-GB'])); + } + + public function testSupportsReturnsTrueWhenAnyStrategySupports(): void + { + $notSupportingStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $notSupportingStrategy + ->expects(self::once()) + ->method('supports') + ->willReturn(false); + + $supportingStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $supportingStrategy + ->expects(self::once()) + ->method('supports') + ->willReturn(true); + + $chain = new ChainContentPublicationStrategy([$notSupportingStrategy, $supportingStrategy]); + + self::assertTrue($chain->supports()); + } + + public function testSupportsReturnsFalseForEmptyChain(): void + { + $chain = new ChainContentPublicationStrategy([]); + + self::assertFalse($chain->supports()); + } + + public function testPublishVersionThrowsLogicExceptionWhenNoStrategySupports(): void + { + $notSupportingStrategy = $this->createMock(ContentPublicationStrategyInterface::class); + $notSupportingStrategy + ->expects(self::once()) + ->method('supports') + ->willReturn(false); + $notSupportingStrategy + ->expects(self::never()) + ->method('publishVersion'); + + $chain = new ChainContentPublicationStrategy([$notSupportingStrategy]); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('No content publication strategy supports the current publication.'); + + $chain->publishVersion($this->createMock(VersionInfo::class)); + } +} diff --git a/tests/lib/Repository/Strategy/Publication/SynchronousContentPublicationStrategyTest.php b/tests/lib/Repository/Strategy/Publication/SynchronousContentPublicationStrategyTest.php new file mode 100644 index 0000000000..27bc27b868 --- /dev/null +++ b/tests/lib/Repository/Strategy/Publication/SynchronousContentPublicationStrategyTest.php @@ -0,0 +1,68 @@ +createStub(ContentService::class) + ); + + self::assertTrue($strategy->supports()); + } + + /** + * @dataProvider providerForTestPublishVersionDelegatesToContentService + * + * @param list|null $translations + * @param list $expectedTranslations + */ + public function testPublishVersionDelegatesToContentService( + ?array $translations, + array $expectedTranslations + ): void { + $versionInfo = $this->createStub(VersionInfo::class); + $content = $this->createStub(Content::class); + + $contentService = $this->createMock(ContentService::class); + $contentService + ->expects(self::once()) + ->method('publishVersion') + ->with(self::identicalTo($versionInfo), $expectedTranslations) + ->willReturn($content); + + $publishArguments = ['versionInfo' => $versionInfo]; + if (null !== $translations) { + $publishArguments['translations'] = $translations; + } + + $result = (new SynchronousContentPublicationStrategy($contentService)) + ->publishVersion(...$publishArguments); + + self::assertSame($content, $result->publishedContent); + } + + /** + * @return iterable|null, list}> + */ + public static function providerForTestPublishVersionDelegatesToContentService(): iterable + { + yield 'explicit translations' => [['ger-DE'], ['ger-DE']]; + yield 'defaults to all translations' => [null, Language::ALL]; + } +}