diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md index 1ceb8fdf211..4c3c226dc96 100644 --- a/src/TwigComponent/CHANGELOG.md +++ b/src/TwigComponent/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.13.0 + +- Add profiler integration: `TwigComponentDataCollector` and debug toolbar templates + ## 2.12.0 - Added a `debug:twig-component` command. diff --git a/src/TwigComponent/composer.json b/src/TwigComponent/composer.json index a5bdb3938c5..36f7e696064 100644 --- a/src/TwigComponent/composer.json +++ b/src/TwigComponent/composer.json @@ -40,6 +40,7 @@ "symfony/framework-bundle": "^5.4|^6.0|^7.0", "symfony/phpunit-bridge": "^6.0|^7.0", "symfony/stimulus-bundle": "^2.9.1", + "symfony/stopwatch": "^5.4|^6.0|^7.0", "symfony/twig-bundle": "^5.4|^6.0|^7.0", "symfony/webpack-encore-bundle": "^1.15" }, diff --git a/src/TwigComponent/config/debug.php b/src/TwigComponent/config/debug.php new file mode 100644 index 00000000000..6fb748d98ef --- /dev/null +++ b/src/TwigComponent/config/debug.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\DependencyInjection\Loader\Configurator; + +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\UX\TwigComponent\DataCollector\TwigComponentDataCollector; +use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; + +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; + +return static function (ContainerConfigurator $container) { + $container->services() + + ->set('ux.twig_component.component_logger_listener', TwigComponentLoggerListener::class) + ->args([ + service('debug.stopwatch')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + + ->set('ux.twig_component.data_collector', TwigComponentDataCollector::class) + ->args([ + service('ux.twig_component.component_logger_listener'), + service('twig'), + ]) + ->tag('data_collector', [ + 'template' => '@TwigComponent/Collector/twig_component.html.twig', + 'id' => 'twig_component', + 'priority' => 256, + ]); +}; diff --git a/src/TwigComponent/src/ComponentRenderer.php b/src/TwigComponent/src/ComponentRenderer.php index 16c5a99d675..c7147be42fb 100644 --- a/src/TwigComponent/src/ComponentRenderer.php +++ b/src/TwigComponent/src/ComponentRenderer.php @@ -76,7 +76,7 @@ public function render(MountedComponent $mounted): string $event->getTemplateIndex(), )->render($event->getVariables()); } finally { - $this->componentStack->pop(); + $mounted = $this->componentStack->pop(); $event = new PostRenderEvent($mounted); $this->dispatcher->dispatch($event); diff --git a/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php new file mode 100644 index 00000000000..75af72f9bd7 --- /dev/null +++ b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\DataCollector; + +use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\UX\TwigComponent\Event\PostRenderEvent; +use Symfony\UX\TwigComponent\Event\PreRenderEvent; +use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; +use Twig\Environment; +use Twig\Error\LoaderError; + +/** + * @author Simon André + */ +class TwigComponentDataCollector extends AbstractDataCollector implements LateDataCollectorInterface +{ + private bool $hasStub; + + public function __construct( + private readonly TwigComponentLoggerListener $logger, + private readonly Environment $twig, + ) { + $this->hasStub = class_exists(ClassStub::class); + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void + { + } + + public function lateCollect(): void + { + $this->collectDataFromLogger(); + $this->data = $this->cloneVar($this->data); + } + + public function getData(): array|Data + { + return $this->data; + } + + public function getName(): string + { + return 'twig_component'; + } + + public function reset(): void + { + $this->logger->reset(); + parent::reset(); + } + + public function getComponents(): array|Data + { + return $this->data['components'] ?? []; + } + + public function getComponentCount(): int + { + return $this->data['component_count'] ?? 0; + } + + public function getPeakMemoryUsage(): int + { + return $this->data['peak_memory_usage'] ?? 0; + } + + public function getRenders(): array|Data + { + return $this->data['renders'] ?? []; + } + + public function getRenderCount(): int + { + return $this->data['render_count'] ?? 0; + } + + public function getRenderTime(): int + { + return $this->data['render_time'] ?? 0; + } + + private function collectDataFromLogger(): void + { + $components = []; + $renders = []; + $ongoingRenders = []; + + foreach ($this->logger->getEvents() as [$event, $profile]) { + if ($event instanceof PreRenderEvent) { + $mountedComponent = $event->getMountedComponent(); + + $metadata = $event->getMetadata(); + $componentName = $metadata->getName(); + $componentClass = $mountedComponent->getComponent()::class; + + $components[$componentName] ??= [ + 'name' => $componentName, + 'class' => $componentClass, + 'class_stub' => $this->hasStub ? new ClassStub($componentClass) : $componentClass, + 'template' => $metadata->getTemplate(), + 'template_path' => $this->resolveTemplatePath($metadata->getTemplate()), // defer ? lazy ? + 'render_count' => 0, + 'render_time' => 0, + ]; + + $renderId = spl_object_id($mountedComponent); + $renders[$renderId] = [ + 'name' => $componentName, + 'class' => $componentClass, + 'is_embed' => $event->isEmbedded(), + 'input_props' => $mountedComponent->getInputProps(), + 'attributes' => $mountedComponent->getAttributes()->all(), + 'variables' => $event->getVariables(), + 'template_index' => $event->getTemplateIndex(), + 'component' => $mountedComponent->getComponent(), + 'depth' => \count($ongoingRenders), + 'children' => [], + 'render_start' => $profile[0], + ]; + + if ($parentId = end($ongoingRenders)) { + $renders[$parentId]['children'][] = $renderId; + } + + $ongoingRenders[$renderId] = $renderId; + continue; + } + + if ($event instanceof PostRenderEvent) { + $mountedComponent = $event->getMountedComponent(); + $componentName = $mountedComponent->getName(); + $renderId = spl_object_id($mountedComponent); + + $renderTime = ($profile[0] - $renders[$renderId]['render_start']) * 1000; + $renders[$renderId] += [ + 'render_end' => $profile[0], + 'render_time' => $renderTime, + 'render_memory' => $profile[1], + ]; + + ++$components[$componentName]['render_count']; + $components[$componentName]['render_time'] += $renderTime; + + unset($ongoingRenders[$renderId]); + } + } + + // Sort by render count DESC + uasort($components, fn ($a, $b) => $b['render_count'] <=> $a['render_count']); + + $this->data['components'] = $components; + $this->data['component_count'] = \count($components); + + $this->data['renders'] = $renders; + $this->data['render_count'] = \count($renders); + $rootRenders = array_filter($renders, fn (array $r) => 0 === $r['depth']); + $this->data['render_time'] = array_sum(array_column($rootRenders, 'render_time')); + + $this->data['peak_memory_usage'] = max([0, ...array_column($renders, 'render_memory')]); + } + + private function resolveTemplatePath(string $logicalName): ?string + { + try { + $source = $this->twig->getLoader()->getSourceContext($logicalName); + } catch (LoaderError) { + return null; + } + + return $source->getPath(); + } +} diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index a01a76bacdd..e8ce315821a 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -11,13 +11,16 @@ namespace Symfony\UX\TwigComponent\DependencyInjection; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Stopwatch\Stopwatch; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Command\ComponentDebugCommand; use Symfony\UX\TwigComponent\ComponentFactory; @@ -41,6 +44,8 @@ final class TwigComponentExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); + if (!isset($container->getParameter('kernel.bundles')['TwigBundle'])) { throw new LogicException('The TwigBundle is not registered in your application. Try running "composer require symfony/twig-bundle".'); } @@ -105,5 +110,9 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in % ]) ->addTag('console.command') ; + + if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) { + $loader->load('debug.php'); + } } } diff --git a/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php b/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php new file mode 100644 index 00000000000..acabac5c522 --- /dev/null +++ b/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Service\ResetInterface; +use Symfony\UX\TwigComponent\Event\PostMountEvent; +use Symfony\UX\TwigComponent\Event\PostRenderEvent; +use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent; +use Symfony\UX\TwigComponent\Event\PreMountEvent; +use Symfony\UX\TwigComponent\Event\PreRenderEvent; + +/** + * @author Simon André + */ +class TwigComponentLoggerListener implements EventSubscriberInterface, ResetInterface +{ + private array $events = []; + + public function __construct(private ?Stopwatch $stopwatch = null) + { + } + + public static function getSubscribedEvents(): array + { + return [ + PreCreateForRenderEvent::class => [ + // High priority: start the stopwatch as soon as possible + ['onPreCreateForRender', 255], + // Low priority: check `event::getRenderedString()` as late as possible + ['onPostCreateForRender', -255], + ], + PreMountEvent::class => ['onPreMount', 255], + PostMountEvent::class => ['onPostMount', -255], + PreRenderEvent::class => ['onPreRender', 255], + PostRenderEvent::class => ['onPostRender', -255], + ]; + } + + public function getEvents(): array + { + return $this->events; + } + + public function onPreCreateForRender(PreCreateForRenderEvent $event): void + { + $this->stopwatch?->start($event->getName(), 'twig_component'); + $this->logEvent($event); + } + + private function logEvent(object $event): void + { + $this->events[] = [$event, [microtime(true), memory_get_usage(true)]]; + } + + public function onPostCreateForRender(PreCreateForRenderEvent $event): void + { + if (\is_string($event->getRenderedString())) { + $this->stopwatch?->stop($event->getName()); + $this->logEvent($event); + } + } + + public function onPreMount(PreMountEvent $event): void + { + $this->logEvent($event); + } + + public function onPostMount(PostMountEvent $event): void + { + $this->logEvent($event); + } + + public function onPreRender(PreRenderEvent $event): void + { + $this->logEvent($event); + } + + public function onPostRender(PostRenderEvent $event): void + { + if ($this->stopwatch?->isStarted($name = $event->getMountedComponent()->getName())) { + $this->stopwatch->stop($name); + } + $this->logEvent($event); + } + + public function reset(): void + { + $this->events = []; + } +} diff --git a/src/TwigComponent/templates/Collector/chevron-down.svg b/src/TwigComponent/templates/Collector/chevron-down.svg new file mode 100644 index 00000000000..359e3da8c70 --- /dev/null +++ b/src/TwigComponent/templates/Collector/chevron-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/TwigComponent/templates/Collector/icon.svg b/src/TwigComponent/templates/Collector/icon.svg new file mode 100644 index 00000000000..36d53025f13 --- /dev/null +++ b/src/TwigComponent/templates/Collector/icon.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/TwigComponent/templates/Collector/twig_component.html.twig b/src/TwigComponent/templates/Collector/twig_component.html.twig new file mode 100644 index 00000000000..71d7167bf19 --- /dev/null +++ b/src/TwigComponent/templates/Collector/twig_component.html.twig @@ -0,0 +1,304 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block page_title 'Twig Components' %} + +{% block head %} + {{ parent() }} + +{% endblock %} + +{% block toolbar %} + {% if collector.renderCount %} + + {% set icon %} + {{ source('@TwigComponent/Collector/icon.svg') }} + {{ collector.renderCount }} + + in + {{ collector.renderTime }} + ms + + {% endset %} + + {% set text %} + {% for _component in collector.components %} +
+ {{ _component.name }} + {{ _component.render_count }} +
+ {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + + {% endif %} +{% endblock %} + +{% block menu %} + + {{ source('@TwigComponent/Collector/icon.svg') }} + Twig Components + +{% endblock %} + +{% block panel %} +

Components

+ {% if not collector.componentCount|default %} +
+

No component were rendered for this request.

+
+ {% else %} +
+
+ {{ _self.metric(collector.componentCount, "Twig Components") }} +
+
+
+ {{ _self.metric(collector.renderCount, "Render Count") }} + {{ _self.metric(collector.renderTime, "Render Time", "ms") }} +
+
+
+ {{ _self.metric((collector.peakMemoryUsage / 1024 / 1024)|number_format(1), "Memory Usage", "MiB") }} +
+
+
+

Components

+ {{ block('table_components') }} +
+
+

Render calls

+ {{ block('table_renders') }} +
+ {% endif %} +{% endblock %} + +{% macro metric(value, label, unit = '') %} +
+ + {{ value }} + {% if unit %} + {{ unit }} + {% endif %} + + + {{- label -}} + +
+{% endmacro %} + +{% block table_components %} + + + + + + + + + + + {% for component in collector.components %} + + + + + + + {% endfor %} + +
+ Name + + Metadata + + Render + Count + + Render + Time +
{{ component.name }} + {% if component.class == 'Symfony\\UX\\TwigComponent\\AnonymousComponent' %} +
[Anonymous]
+ {% else %} + {{ profiler_dump(component.class_stub) }} + {% endif %} + {% if component.template_path %} + + {{- component.template -}} + + {% else %} + {{ component.template }} + {% endif %} +
{{ component.render_count }} + {{- component.render_time|number_format(2) -}} + ms +
+{% endblock %} + +{% block table_renders %} +
+ {% set _memory = null %} + {% for render in collector.renders %} + + + + + + + + + + + + + + + + + + + + + + + + +
{{ render.depth ? source('@TwigComponent/Collector/chevron-down.svg') }}{{ render.name }} + {% if render.class == 'Symfony\\UX\\TwigComponent\\AnonymousComponent' %} +
[Anonymous]
+ {% else %} + {{ render.class }} + {% endif %} +
+ {% set _render_memory = render.render_memory|default(0) / 1024 / 1024 %} + + {{- _render_memory|number_format(1) -}} + + MiB + {% set _memory = _render_memory %} + + {{ render.render_time|number_format(2) }} + ms + + +
Input props{{ profiler_dump(render.input_props) }}
Attributes{{ profiler_dump(render.attributes) }}
Component{{ profiler_dump(render.component) }}
+ {% endfor %} +
+{% endblock %} diff --git a/src/TwigComponent/tests/Fixtures/User.php b/src/TwigComponent/tests/Fixtures/User.php index eb25026072a..0702e328d3e 100644 --- a/src/TwigComponent/tests/Fixtures/User.php +++ b/src/TwigComponent/tests/Fixtures/User.php @@ -7,7 +7,8 @@ class User public function __construct( private readonly string $name, private readonly string $email, - ) {} + ) { + } public function getName(): string { @@ -18,4 +19,4 @@ public function getEmail(): string { return $this->email; } -} \ No newline at end of file +} diff --git a/src/TwigComponent/tests/Unit/DataCollector/TwigComponentDataCollectorTest.php b/src/TwigComponent/tests/Unit/DataCollector/TwigComponentDataCollectorTest.php new file mode 100644 index 00000000000..0c236508ffa --- /dev/null +++ b/src/TwigComponent/tests/Unit/DataCollector/TwigComponentDataCollectorTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Tests\Unit\DataCollector; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\UX\TwigComponent\DataCollector\TwigComponentDataCollector; +use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; +use Twig\Environment; + +/** + * @author Simon André + */ +class TwigComponentDataCollectorTest extends TestCase +{ + public function testCollectDoesNothing(): void + { + $logger = new TwigComponentLoggerListener(); + $twig = $this->createMock(Environment::class); + $dataCollector = new TwigComponentDataCollector($logger, $twig); + + $this->assertSame([], $dataCollector->getData()); + + $dataCollector->collect(new Request(), new Response()); + $this->assertSame([], $dataCollector->getData()); + } + + public function testLateCollect(): void + { + $logger = new TwigComponentLoggerListener(); + $twig = $this->createMock(Environment::class); + $dataCollector = new TwigComponentDataCollector($logger, $twig); + + $dataCollector->lateCollect(); + + $this->assertSame(0, $dataCollector->getComponentCount()); + $this->assertIsIterable($dataCollector->getComponents()); + $this->assertEmpty($dataCollector->getComponents()); + + $this->assertSame(0, $dataCollector->getRenderCount()); + $this->assertIsIterable($dataCollector->getRenders()); + $this->assertEmpty($dataCollector->getRenders()); + + $this->assertSame(0, $dataCollector->getRenderTime()); + } + + public function testReset(): void + { + $logger = new TwigComponentLoggerListener(); + $twig = $this->createMock(Environment::class); + $dataCollector = new TwigComponentDataCollector($logger, $twig); + + $dataCollector->lateCollect(); + $this->assertNotSame([], $dataCollector->getData()); + + $dataCollector->reset(); + $this->assertSame([], $dataCollector->getData()); + } + + public function testGetName(): void + { + $logger = new TwigComponentLoggerListener(); + $twig = $this->createMock(Environment::class); + $dataCollector = new TwigComponentDataCollector($logger, $twig); + + $this->assertEquals('twig_component', $dataCollector->getName()); + } +} diff --git a/src/TwigComponent/tests/Unit/DependencyInjection/TwigComponentExtensionTest.php b/src/TwigComponent/tests/Unit/DependencyInjection/TwigComponentExtensionTest.php new file mode 100644 index 00000000000..bf4751e88d2 --- /dev/null +++ b/src/TwigComponent/tests/Unit/DependencyInjection/TwigComponentExtensionTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Test\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\UX\TwigComponent\DependencyInjection\TwigComponentExtension; +use Symfony\UX\TwigComponent\TwigComponentBundle; + +/** + * @author Simon André + */ +class TwigComponentExtensionTest extends TestCase +{ + public function testDataCollectorWithDebugMode() + { + $container = $this->createContainer(); + $container->registerExtension(new TwigComponentExtension()); + $container->loadFromExtension('twig_component', []); + $this->compileContainer($container); + + $this->assertTrue($container->hasDefinition('ux.twig_component.data_collector')); + } + + public function testDataCollectorWithoutDebugMode() + { + $container = $this->createContainer(); + $container->setParameter('kernel.debug', false); + $container->registerExtension(new TwigComponentExtension()); + $container->loadFromExtension('twig_component', []); + $this->compileContainer($container); + + $this->assertFalse($container->hasDefinition('ux.twig_component.data_collector')); + } + + private function createContainer() + { + $container = new ContainerBuilder(new ParameterBag([ + 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, + 'kernel.charset' => 'UTF-8', + 'kernel.debug' => true, + 'kernel.project_dir' => __DIR__, + 'kernel.bundles' => [ + 'TwigBundle' => new class() {}, + 'TwigComponentBundle' => TwigComponentBundle::class, + ], + ])); + + return $container; + } + + private function compileContainer(ContainerBuilder $container) + { + $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); + $container->compile(); + } +} diff --git a/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php b/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php new file mode 100644 index 00000000000..9f2b3c81e3c --- /dev/null +++ b/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\Tests\Unit\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\TwigComponent\ComponentAttributes; +use Symfony\UX\TwigComponent\ComponentMetadata; +use Symfony\UX\TwigComponent\Event\PostMountEvent; +use Symfony\UX\TwigComponent\Event\PostRenderEvent; +use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent; +use Symfony\UX\TwigComponent\Event\PreMountEvent; +use Symfony\UX\TwigComponent\Event\PreRenderEvent; +use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; +use Symfony\UX\TwigComponent\MountedComponent; + +/** + * @author Simon André + */ +class TwigComponentLoggerListenerTest extends TestCase +{ + public function testLoggerStoreEvents(): void + { + $logger = new TwigComponentLoggerListener(); + $this->assertSame([], $logger->getEvents()); + + $eventA = new PreCreateForRenderEvent('a'); + $logger->onPreCreateForRender($eventA); + + $eventB = new PreCreateForRenderEvent('b'); + $logger->onPreCreateForRender($eventB); + + $eventC = new PreMountEvent(new \stdClass(), []); + $logger->onPreMount($eventC); + $eventD = new PostMountEvent(new \stdClass(), []); + $logger->onPostMount($eventD); + + $mounted = new MountedComponent('foo', new \stdClass(), new ComponentAttributes([])); + $eventE = new PreRenderEvent($mounted, new ComponentMetadata(['template' => 'bar']), []); + $logger->onPreRender($eventE); + $eventF = new PostRenderEvent($mounted); + $logger->onPostRender($eventF); + + $this->assertSame([$eventA, $eventB, $eventC, $eventD, $eventE, $eventF], array_column($logger->getEvents(), 0)); + } + + public function testLoggerReset(): void + { + $logger = new TwigComponentLoggerListener(); + + $logger->onPreCreateForRender(new PreCreateForRenderEvent('foo')); + $this->assertNotSame([], $logger->getEvents()); + + $logger->reset(); + $this->assertSame([], $logger->getEvents()); + } +}