diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 43e60a3254..32145a6105 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -62664,3 +62664,8 @@ parameters: message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Specification\\\\Content\\\\ContentTypeSpecificationTest\\:\\:providerForIsSatisfiedBy\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: tests/lib/Specification/Content/ContentTypeSpecificationTest.php + + - + message: "#^Method Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Events\\\\Content\\\\BeforeLoadContentEvent\\:\\:getContent\\(\\) should return Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\Content\\\\Content but returns Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\Content\\\\Content\\|null\\.$#" + count: 1 + path: src/contracts/Repository/Events/Content/BeforeLoadContentEvent.php \ No newline at end of file diff --git a/src/contracts/Repository/Events/Content/BeforeLoadContentEvent.php b/src/contracts/Repository/Events/Content/BeforeLoadContentEvent.php new file mode 100644 index 0000000000..f92029d286 --- /dev/null +++ b/src/contracts/Repository/Events/Content/BeforeLoadContentEvent.php @@ -0,0 +1,84 @@ +contentId = $contentId; + $this->languages = $languages; + $this->versionNo = $versionNo; + $this->useAlwaysAvailable = $useAlwaysAvailable; + } + + public function getContentId(): int + { + return $this->contentId; + } + + /** + * @return string[]|null + */ + public function getLanguages(): ?array + { + return $this->languages; + } + + public function getVersionNo(): ?int + { + return $this->versionNo; + } + + public function getUseAlwaysAvailable(): bool + { + return $this->useAlwaysAvailable; + } + + public function getContent(): Content + { + if (!$this->hasContent()) { + throw new UnexpectedValueException(sprintf('Return value is not set or not of type %s. Check hasContent() or set it using setContent() before you call the getter.', Content::class)); + } + + return $this->content; + } + + public function setContent(?Content $content): void + { + $this->content = $content; + } + + public function hasContent(): bool + { + return $this->content instanceof Content; + } +} diff --git a/src/lib/Event/ContentService.php b/src/lib/Event/ContentService.php index dee89f597c..e17e2cf34f 100644 --- a/src/lib/Event/ContentService.php +++ b/src/lib/Event/ContentService.php @@ -20,6 +20,7 @@ use Ibexa\Contracts\Core\Repository\Events\Content\BeforeDeleteTranslationEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeDeleteVersionEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeHideContentEvent; +use Ibexa\Contracts\Core\Repository\Events\Content\BeforeLoadContentEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforePublishVersionEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeRevealContentEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeUpdateContentEvent; @@ -380,6 +381,31 @@ public function revealContent(ContentInfo $contentInfo): void new RevealContentEvent(...$eventData) ); } + + public function loadContent( + int $contentId, + array $languages = null, + ?int $versionNo = null, + bool $useAlwaysAvailable = true + ): Content { + $eventData = [ + $contentId, + $languages, + $versionNo, + $useAlwaysAvailable, + ]; + + $beforeEvent = new BeforeLoadContentEvent(...$eventData); + + $this->eventDispatcher->dispatch($beforeEvent); + if ($beforeEvent->isPropagationStopped()) { + return $beforeEvent->getContent(); + } + + return $beforeEvent->hasContent() + ? $beforeEvent->getContent() + : $this->innerService->loadContent($contentId, $languages, $versionNo, $useAlwaysAvailable); + } } class_alias(ContentService::class, 'eZ\Publish\Core\Event\ContentService'); diff --git a/src/lib/Persistence/Legacy/Content/Mapper.php b/src/lib/Persistence/Legacy/Content/Mapper.php index b7a034e864..29d1741075 100644 --- a/src/lib/Persistence/Legacy/Content/Mapper.php +++ b/src/lib/Persistence/Legacy/Content/Mapper.php @@ -379,7 +379,7 @@ public function extractContentInfoFromRow( $contentInfo->id = (int)$row["{$prefix}id"]; $contentInfo->name = (string)$row["{$prefix}name"]; $contentInfo->contentTypeId = (int)$row["{$prefix}contentclass_id"]; - $contentInfo->contentTypeIdentifier = $row['content_type_identifier']; + $contentInfo->contentTypeIdentifier = (string)$row['content_type_identifier']; $contentInfo->sectionId = (int)$row["{$prefix}section_id"]; $contentInfo->currentVersionNo = (int)$row["{$prefix}current_version"]; $contentInfo->ownerId = (int)$row["{$prefix}owner_id"]; diff --git a/tests/lib/Event/AbstractServiceTest.php b/tests/lib/Event/AbstractServiceTest.php index 8dd461c73c..6f0633b599 100644 --- a/tests/lib/Event/AbstractServiceTest.php +++ b/tests/lib/Event/AbstractServiceTest.php @@ -15,11 +15,14 @@ abstract class AbstractServiceTest extends TestCase { - public function getEventDispatcher(string $beforeEventName, string $eventName): TraceableEventDispatcher + public function getEventDispatcher(string $beforeEventName, ?string $eventName): TraceableEventDispatcher { $eventDispatcher = new EventDispatcher(); $eventDispatcher->addListener($beforeEventName, static function (BeforeEvent $event) {}); - $eventDispatcher->addListener($eventName, static function (AfterEvent $event) {}); + if ($eventName !== null) { + $eventDispatcher->addListener($eventName, static function (AfterEvent $event) { + }); + } return new TraceableEventDispatcher( $eventDispatcher, diff --git a/tests/lib/Event/ContentServiceTest.php b/tests/lib/Event/ContentServiceTest.php index 3a5761249b..f85d9919e1 100644 --- a/tests/lib/Event/ContentServiceTest.php +++ b/tests/lib/Event/ContentServiceTest.php @@ -17,6 +17,7 @@ use Ibexa\Contracts\Core\Repository\Events\Content\BeforeDeleteTranslationEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeDeleteVersionEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeHideContentEvent; +use Ibexa\Contracts\Core\Repository\Events\Content\BeforeLoadContentEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforePublishVersionEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeRevealContentEvent; use Ibexa\Contracts\Core\Repository\Events\Content\BeforeUpdateContentEvent; @@ -1153,6 +1154,111 @@ public function testRevealContentStopPropagationInBeforeEvents() [RevealContentEvent::class, 0], ]); } + + public function testLoadContentEvents(): void + { + $traceableEventDispatcher = $this->getEventDispatcher( + BeforeLoadContentEvent::class, + null + ); + + $parameters = [ + 2, + [], + null, + true, + ]; + + $content = $this->createMock(Content::class); + $innerServiceMock = $this->createMock(ContentServiceInterface::class); + $innerServiceMock->method('loadContent')->willReturn($content); + + $service = new ContentService($innerServiceMock, $traceableEventDispatcher); + $result = $service->loadContent(...$parameters); + + $calledListeners = $this->getListenersStack($traceableEventDispatcher->getCalledListeners()); + + $this->assertSame($content, $result); + $this->assertSame($calledListeners, [ + [BeforeLoadContentEvent::class, 0], + ]); + $this->assertSame([], $traceableEventDispatcher->getNotCalledListeners()); + } + + public function testReturnLoadContentResultInBeforeEvents(): void + { + $traceableEventDispatcher = $this->getEventDispatcher( + BeforeLoadContentEvent::class, + null + ); + + $parameters = [ + 2, + [], + null, + true, + ]; + + $content = $this->createMock(Content::class); + $eventContent = $this->createMock(Content::class); + $innerServiceMock = $this->createMock(ContentServiceInterface::class); + $innerServiceMock->method('loadContent')->willReturn($content); + + $traceableEventDispatcher->addListener(BeforeLoadContentEvent::class, static function (BeforeLoadContentEvent $event) use ($eventContent) { + $event->setContent($eventContent); + }, 10); + + $service = new ContentService($innerServiceMock, $traceableEventDispatcher); + $result = $service->loadContent(...$parameters); + + $calledListeners = $this->getListenersStack($traceableEventDispatcher->getCalledListeners()); + + $this->assertSame($eventContent, $result); + $this->assertSame($calledListeners, [ + [BeforeLoadContentEvent::class, 10], + [BeforeLoadContentEvent::class, 0], + ]); + $this->assertSame([], $traceableEventDispatcher->getNotCalledListeners()); + } + + public function testLoadContentStopPropagationInBeforeEvents(): void + { + $traceableEventDispatcher = $this->getEventDispatcher( + BeforeLoadContentEvent::class, + null + ); + + $parameters = [ + 2, + [], + null, + true, + ]; + + $content = $this->createMock(Content::class); + $eventContent = $this->createMock(Content::class); + $innerServiceMock = $this->createMock(ContentServiceInterface::class); + $innerServiceMock->method('loadContent')->willReturn($content); + + $traceableEventDispatcher->addListener(BeforeLoadContentEvent::class, static function (BeforeLoadContentEvent $event) use ($eventContent) { + $event->setContent($eventContent); + $event->stopPropagation(); + }, 10); + + $service = new ContentService($innerServiceMock, $traceableEventDispatcher); + $result = $service->loadContent(...$parameters); + + $calledListeners = $this->getListenersStack($traceableEventDispatcher->getCalledListeners()); + $notCalledListeners = $this->getListenersStack($traceableEventDispatcher->getNotCalledListeners()); + + $this->assertSame($eventContent, $result); + $this->assertSame($calledListeners, [ + [BeforeLoadContentEvent::class, 10], + ]); + $this->assertSame($notCalledListeners, [ + [BeforeLoadContentEvent::class, 0], + ]); + } } class_alias(ContentServiceTest::class, 'eZ\Publish\Core\Event\Tests\ContentServiceTest');