Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bundle/Core/DependencyInjection/IbexaCoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Core\Repository\Strategy\ContentPublication;

use Ibexa\Contracts\Core\Repository\Values\Content\Content;

/**
* @experimental This contract may change in future.
*/
final readonly class ContentPublicationResult
{
public function __construct(
public ?Content $publishedContent,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Core\Repository\Strategy\ContentPublication;

use Ibexa\Contracts\Core\Repository\Values\Content\Language;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;

/**
* Entry point for publishing a content version, hiding whether the publication happens
* synchronously (in-request) or asynchronously (queued background work).
*
* Strategies are registered with the "ibexa.repository.content.publication_strategy" service tag
* and consulted in priority order; the first one supporting the current repository executes.
*/
interface ContentPublicationStrategyInterface
{
public function supports(): bool;

/**
* @param list<string> $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
Comment thread
bnowak marked this conversation as resolved.
*/
public function publishVersion(VersionInfo $versionInfo, array $translations = Language::ALL): ContentPublicationResult;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Repository\Strategy\ContentPublication;

use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationResult;
use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationStrategyInterface;
use Ibexa\Contracts\Core\Repository\Values\Content\Language;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;
use LogicException;

/**
* Dispatches publication to the first supporting strategy, in priority order.
* Exactly one strategy executes per publication.
*
* @internal Meant for internal use by Repository
*/
final readonly class ChainContentPublicationStrategy implements ContentPublicationStrategyInterface
{
/**
* @param iterable<ContentPublicationStrategyInterface> $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,
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Repository\Strategy\ContentPublication;

use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationResult;
use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationStrategyInterface;
use Ibexa\Contracts\Core\Repository\Values\Content\Language;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;

/**
* Synchronous publication strategy: publishes the version inside the current request.
* Always-applicable fallback, registered with the lowest priority.
*
* @internal
*/
final readonly class SynchronousContentPublicationStrategy implements ContentPublicationStrategyInterface
{
public function __construct(
private ContentService $contentService,
) {
}

public function supports(): bool
{
return true;
}

public function publishVersion(VersionInfo $versionInfo, array $translations = Language::ALL): ContentPublicationResult
{
return new ContentPublicationResult(
$this->contentService->publishVersion($versionInfo, $translations)
);
}
}
22 changes: 22 additions & 0 deletions src/lib/Resources/settings/content_publication.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Core\Repository\Strategy\Publication;

use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationResult;
use Ibexa\Contracts\Core\Repository\Strategy\ContentPublication\ContentPublicationStrategyInterface;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;
use Ibexa\Core\Repository\Strategy\ContentPublication\ChainContentPublicationStrategy;
use LogicException;
use PHPUnit\Framework\TestCase;

final class ChainContentPublicationStrategyTest extends TestCase
{
public function testPublishVersionExecutesFirstSupportingStrategy(): void
{
$versionInfo = $this->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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Core\Repository\Strategy\Publication;

use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Ibexa\Contracts\Core\Repository\Values\Content\Language;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;
use Ibexa\Core\Repository\Strategy\ContentPublication\SynchronousContentPublicationStrategy;
use PHPUnit\Framework\TestCase;

final class SynchronousContentPublicationStrategyTest extends TestCase
{
public function testSupportsAlwaysReturnsTrue(): void
{
$strategy = new SynchronousContentPublicationStrategy(
$this->createStub(ContentService::class)
);

self::assertTrue($strategy->supports());
}

/**
* @dataProvider providerForTestPublishVersionDelegatesToContentService
*
* @param list<string>|null $translations
* @param list<string> $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<string, array{list<string>|null, list<string>}>
*/
public static function providerForTestPublishVersionDelegatesToContentService(): iterable
{
yield 'explicit translations' => [['ger-DE'], ['ger-DE']];
yield 'defaults to all translations' => [null, Language::ALL];
}
}