From fb0a668c30ce588cbaec66163e60799724e8c9d9 Mon Sep 17 00:00:00 2001 From: Sebastian Michaelsen Date: Wed, 4 Mar 2026 12:40:26 +0100 Subject: [PATCH] feat: introduce ComponentWasRendered lifecycle event --- .../WebcomponentContentObject.php | 4 +- .../Helpers/ComponentRenderingHelper.php | 2 +- Classes/Dto/Events/ComponentWasRendered.php | 28 ++++++++ .../Dto/Events/ComponentWillBeRendered.php | 1 + Classes/Rendering/ComponentRenderer.php | 25 ++++++- Classes/ViewHelpers/RenderViewHelper.php | 2 +- .../Unit/Rendering/ComponentRendererTest.php | 67 ++++++++++++++++--- 7 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 Classes/Dto/Events/ComponentWasRendered.php diff --git a/Classes/ContentObject/WebcomponentContentObject.php b/Classes/ContentObject/WebcomponentContentObject.php index 93a514d..cd8ca35 100644 --- a/Classes/ContentObject/WebcomponentContentObject.php +++ b/Classes/ContentObject/WebcomponentContentObject.php @@ -24,6 +24,7 @@ public function __construct( public function render($conf = []): string { $contentObjectRenderer = $this->getContentObjectRenderer(); + $renderComponentClassName = ''; /** @var array $record */ $record = $contentObjectRenderer->data; @@ -47,6 +48,7 @@ public function render($conf = []): string } $componentClassName = $contentObjectRenderer->stdWrapValue('component', $conf, null); if (is_string($componentClassName) && $componentClassName !== '') { + $renderComponentClassName = $componentClassName; try { $componentRenderingData = $this->componentRenderer->evaluateComponent($inputData, $componentClassName, $contentObjectRenderer); } catch (AssertionFailedException $e) { @@ -59,7 +61,7 @@ public function render($conf = []): string $componentRenderingData = $this->evaluateTypoScriptConfiguration($componentRenderingData, $contentObjectRenderer, $conf); - $markup = $this->componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer); + $markup = $this->componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer, $renderComponentClassName); // apply stdWrap if (is_array($conf['stdWrap.'] ?? null)) { diff --git a/Classes/DataProviding/Helpers/ComponentRenderingHelper.php b/Classes/DataProviding/Helpers/ComponentRenderingHelper.php index be9e818..296751f 100644 --- a/Classes/DataProviding/Helpers/ComponentRenderingHelper.php +++ b/Classes/DataProviding/Helpers/ComponentRenderingHelper.php @@ -50,6 +50,6 @@ public function evaluateAndRenderComponent(string $componentClassName, ?InputDat $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $contentObjectRenderer->start($inputData->record, $inputData->tableName); - return $this->componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer); + return $this->componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer, $componentClassName); } } diff --git a/Classes/Dto/Events/ComponentWasRendered.php b/Classes/Dto/Events/ComponentWasRendered.php new file mode 100644 index 0000000..9f715e7 --- /dev/null +++ b/Classes/Dto/Events/ComponentWasRendered.php @@ -0,0 +1,28 @@ +markup; + } + + public function setMarkup(string $markup): void + { + $this->markup = $markup; + } +} diff --git a/Classes/Dto/Events/ComponentWillBeRendered.php b/Classes/Dto/Events/ComponentWillBeRendered.php index e5fe2d7..66acc4b 100644 --- a/Classes/Dto/Events/ComponentWillBeRendered.php +++ b/Classes/Dto/Events/ComponentWillBeRendered.php @@ -22,6 +22,7 @@ final class ComponentWillBeRendered public function __construct( ComponentRenderingData $componentRenderingData, public readonly ContentObjectRenderer $contentObjectRenderer, + public readonly string $componentClassName, ) { $this->componentRenderingData = $componentRenderingData; } diff --git a/Classes/Rendering/ComponentRenderer.php b/Classes/Rendering/ComponentRenderer.php index 72ac7a0..280396c 100644 --- a/Classes/Rendering/ComponentRenderer.php +++ b/Classes/Rendering/ComponentRenderer.php @@ -9,6 +9,7 @@ use Sinso\Webcomponents\DataProviding\Traits\Assert; use Sinso\Webcomponents\Dto\ComponentRenderingData; use Sinso\Webcomponents\Dto\Events\ComponentEvaluated; +use Sinso\Webcomponents\Dto\Events\ComponentWasRendered; use Sinso\Webcomponents\Dto\Events\ComponentWillBeRendered; use Sinso\Webcomponents\Dto\InputData; use Psr\EventDispatcher\EventDispatcherInterface; @@ -17,6 +18,10 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; +/** + * @internal Render content elements via \Sinso\Webcomponents\ContentObject\WebcomponentContentObject. + * For rendering from PHP context use \Sinso\Webcomponents\DataProviding\Helpers\ComponentRenderingHelper. + */ class ComponentRenderer { use Assert; @@ -25,11 +30,25 @@ public function __construct( private readonly EventDispatcherInterface $eventDispatcher, ) {} - public function renderComponent(ComponentRenderingData $componentRenderingData, ContentObjectRenderer $contentObjectRenderer, ?TagBuilder $tagBuilder = null): string + public function renderComponent( + ComponentRenderingData $componentRenderingData, + ContentObjectRenderer $contentObjectRenderer, + string $componentClassName, + ?TagBuilder $tagBuilder = null, + ): string { - $event = new ComponentWillBeRendered($componentRenderingData, $contentObjectRenderer); + $event = new ComponentWillBeRendered($componentRenderingData, $contentObjectRenderer, $componentClassName); $this->eventDispatcher->dispatch($event); - return $this->renderMarkup($event->getComponentRenderingData(), $tagBuilder); + + $markup = $this->renderMarkup($event->getComponentRenderingData(), $tagBuilder); + $componentWasRenderedEvent = new ComponentWasRendered( + $markup, + $event->getComponentRenderingData(), + $contentObjectRenderer, + $componentClassName, + ); + $this->eventDispatcher->dispatch($componentWasRenderedEvent); + return $componentWasRenderedEvent->getMarkup(); } public function evaluateComponent(InputData $inputData, string $componentClassName, ?ContentObjectRenderer $contentObjectRenderer = null): ComponentRenderingData diff --git a/Classes/ViewHelpers/RenderViewHelper.php b/Classes/ViewHelpers/RenderViewHelper.php index aa0d89c..7daaa2c 100644 --- a/Classes/ViewHelpers/RenderViewHelper.php +++ b/Classes/ViewHelpers/RenderViewHelper.php @@ -49,7 +49,7 @@ public function render(): string return $e->getRenderingPlaceholder(); } - return $componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer, $this->tag); + return $componentRenderer->renderComponent($componentRenderingData, $contentObjectRenderer, $componentClassName, $this->tag); } protected function getContentObjectRenderer(): ContentObjectRenderer diff --git a/Tests/Unit/Rendering/ComponentRendererTest.php b/Tests/Unit/Rendering/ComponentRendererTest.php index 4e79c15..561efa3 100644 --- a/Tests/Unit/Rendering/ComponentRendererTest.php +++ b/Tests/Unit/Rendering/ComponentRendererTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Psr\EventDispatcher\EventDispatcherInterface; use Sinso\Webcomponents\Dto\ComponentRenderingData; +use Sinso\Webcomponents\Dto\Events\ComponentWasRendered; use Sinso\Webcomponents\Dto\Events\ComponentWillBeRendered; use Sinso\Webcomponents\Rendering\ComponentRenderer; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; @@ -35,7 +36,11 @@ public function rendersComponentAccordingToProvidedData(): void ->withTagName('my-element') ->withTagProperties(['class' => 'foo']) ->withTagContent('bar'); - $result = $this->subject->renderComponent($componentRenderer, $this->createMock(ContentObjectRenderer::class)); + $result = $this->subject->renderComponent( + $componentRenderer, + $this->createMock(ContentObjectRenderer::class), + 'My\\Component\\ClassName', + ); // should roughly look like: bar, but don't care about whitespaces self::assertMatchesRegularExpression( @@ -47,13 +52,24 @@ public function rendersComponentAccordingToProvidedData(): void #[Test] public function inputFromComponentWillBeRenderedEventListenersIsConsidered(): void { - $this->eventDispatcher->expects(self::once())->method('dispatch')->willReturnCallback(function ($event) { - /** @var ComponentWillBeRendered $event */ - self::assertInstanceOf(ComponentWillBeRendered::class, $event); - $event->setComponentRenderingData( - $event->getComponentRenderingData() - ->withTagProperty('class', 'baz') - ); + $eventCallCount = 0; + $this->eventDispatcher->expects(self::exactly(2))->method('dispatch')->willReturnCallback(function ($event) use (&$eventCallCount) { + $eventCallCount++; + if ($eventCallCount === 1) { + /** @var ComponentWillBeRendered $event */ + self::assertInstanceOf(ComponentWillBeRendered::class, $event); + self::assertSame('My\\Component\\ClassName', $event->componentClassName); + $event->setComponentRenderingData( + $event->getComponentRenderingData() + ->withTagProperty('class', 'baz') + ); + } + + if ($eventCallCount === 2) { + self::assertInstanceOf(ComponentWasRendered::class, $event); + self::assertSame('My\\Component\\ClassName', $event->componentClassName); + } + return $event; }); @@ -61,7 +77,11 @@ public function inputFromComponentWillBeRenderedEventListenersIsConsidered(): vo ->withTagName('my-element') ->withTagProperties(['class' => 'foo']) ->withTagContent('bar'); - $result = $this->subject->renderComponent($componentRenderer, $this->createMock(ContentObjectRenderer::class)); + $result = $this->subject->renderComponent( + $componentRenderer, + $this->createMock(ContentObjectRenderer::class), + 'My\\Component\\ClassName', + ); // should roughly look like: bar, but don't care about whitespaces self::assertMatchesRegularExpression( @@ -69,4 +89,33 @@ public function inputFromComponentWillBeRenderedEventListenersIsConsidered(): vo $result, ); } + + #[Test] + public function inputFromComponentWasRenderedEventListenersIsConsidered(): void + { + $eventCallCount = 0; + $this->eventDispatcher->expects(self::exactly(2))->method('dispatch')->willReturnCallback(function ($event) use (&$eventCallCount) { + $eventCallCount++; + if ($eventCallCount === 2) { + /** @var ComponentWasRendered $event */ + self::assertInstanceOf(ComponentWasRendered::class, $event); + self::assertMatchesRegularExpression('/\s*bar\s*<\/my-element>/', $event->getMarkup()); + $event->setMarkup('from-event'); + } + + return $event; + }); + + $componentRenderer = (new ComponentRenderingData()) + ->withTagName('my-element') + ->withTagProperties(['class' => 'foo']) + ->withTagContent('bar'); + $result = $this->subject->renderComponent( + $componentRenderer, + $this->createMock(ContentObjectRenderer::class), + 'My\\Component\\ClassName', + ); + + self::assertSame('from-event', $result); + } }