diff --git a/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php b/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php index 1396dfe0ace..19928e8bd95 100644 --- a/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php +++ b/src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php @@ -28,17 +28,17 @@ public function onPostMount(PostMountEvent $event): void $data = $event->getData(); if (\array_key_exists('defer', $data)) { $event->addExtraMetadata('defer', true); - unset($event->getData()['defer']); + unset($data['defer']); } if (\array_key_exists('loading-template', $data)) { $event->addExtraMetadata('loading-template', $data['loading-template']); - unset($event->getData()['loading-template']); + unset($data['loading-template']); } if (\array_key_exists('loading-tag', $data)) { $event->addExtraMetadata('loading-tag', $data['loading-tag']); - unset($event->getData()['loading-tag']); + unset($data['loading-tag']); } $event->setData($data); diff --git a/src/TwigComponent/src/ComponentRenderer.php b/src/TwigComponent/src/ComponentRenderer.php index 7a70039f1d9..4a617da8682 100644 --- a/src/TwigComponent/src/ComponentRenderer.php +++ b/src/TwigComponent/src/ComponentRenderer.php @@ -64,16 +64,23 @@ public function render(MountedComponent $mounted): string $event = $this->preRender($mounted); + $variables = $event->getVariables(); + // see ComponentNode. When rendering an individual embedded component, + // *not* through its parent, we need to set the parent template. + if ($event->getTemplateIndex()) { + $variables['__parent__'] = $event->getParentTemplateForEmbedded(); + } + try { if ($this->twig::MAJOR_VERSION < 3) { - return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($event->getVariables()); + return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($variables); } return $this->twig->loadTemplate( $this->twig->getTemplateClass($event->getTemplate()), $event->getTemplate(), $event->getTemplateIndex(), - )->render($event->getVariables()); + )->render($variables); } finally { $mounted = $this->componentStack->pop(); @@ -82,7 +89,7 @@ public function render(MountedComponent $mounted): string } } - public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array + public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent { $context[PreRenderEvent::EMBEDDED] = true; @@ -92,13 +99,7 @@ public function embeddedContext(string $name, array $props, array $context, stri $this->componentStack->push($mounted); - $embeddedContext = $this->preRender($mounted, $context)->getVariables(); - - if (!isset($embeddedContext['outerBlocks'])) { - $embeddedContext['outerBlocks'] = new BlockStack(); - } - - return $embeddedContext; + return $this->preRender($mounted, $context); } public function finishEmbeddedComponentRender(): void diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index 0993e4e3606..68b4e7d4114 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -102,7 +102,6 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in % $container->register('ux.twig_component.twig.component_extension', ComponentExtension::class) ->addTag('twig.extension') ->addTag('container.service_subscriber', ['key' => ComponentRenderer::class, 'id' => 'ux.twig_component.component_renderer']) - ->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory']) ; $container->register('ux.twig_component.twig.lexer', ComponentLexer::class); diff --git a/src/TwigComponent/src/Event/PreRenderEvent.php b/src/TwigComponent/src/Event/PreRenderEvent.php index 7077d567ed1..ac286e479b6 100644 --- a/src/TwigComponent/src/Event/PreRenderEvent.php +++ b/src/TwigComponent/src/Event/PreRenderEvent.php @@ -23,8 +23,13 @@ final class PreRenderEvent extends Event /** @internal */ public const EMBEDDED = '__embedded'; + /** + * Only relevant when rendering a specific embedded component. + * This is the "component template" that the embedded component + * should extend. + */ + private string $parentTemplateForEmbedded; private string $template; - private ?int $templateIndex = null; /** @@ -36,6 +41,7 @@ public function __construct( private array $variables ) { $this->template = $this->metadata->getTemplate(); + $this->parentTemplateForEmbedded = $this->template; } public function isEmbedded(): bool @@ -58,6 +64,10 @@ public function setTemplate(string $template, int $index = null): self { $this->template = $template; $this->templateIndex = $index; + // only if we are *not* targeting an embedded component, change the parent template + if (null === $index) { + $this->parentTemplateForEmbedded = $template; + } return $this; } @@ -70,6 +80,11 @@ public function getTemplateIndex(): ?int return $this->templateIndex; } + public function getParentTemplateForEmbedded(): string + { + return $this->parentTemplateForEmbedded; + } + public function getComponent(): object { return $this->mounted->getComponent(); diff --git a/src/TwigComponent/src/Twig/ComponentExtension.php b/src/TwigComponent/src/Twig/ComponentExtension.php index 41b674637d4..1df860e96f4 100644 --- a/src/TwigComponent/src/Twig/ComponentExtension.php +++ b/src/TwigComponent/src/Twig/ComponentExtension.php @@ -13,8 +13,8 @@ use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; -use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentRenderer; +use Symfony\UX\TwigComponent\Event\PreRenderEvent; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -34,7 +34,6 @@ public static function getSubscribedServices(): array { return [ ComponentRenderer::class, - ComponentFactory::class, ]; } @@ -48,7 +47,7 @@ public function getFunctions(): array public function getTokenParsers(): array { return [ - new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)), + new ComponentTokenParser(), new PropsTokenParser(), ]; } @@ -62,7 +61,7 @@ public function render(string $name, array $props = []): string } } - public function preRender(string $name, array $props): ?string + public function extensionPreCreateForRender(string $name, array $props): ?string { try { return $this->container->get(ComponentRenderer::class)->preCreateForRender($name, $props); @@ -71,10 +70,10 @@ public function preRender(string $name, array $props): ?string } } - public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array + public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent { try { - return $this->container->get(ComponentRenderer::class)->embeddedContext($name, $props, $context, $hostTemplateName, $index); + return $this->container->get(ComponentRenderer::class)->startEmbeddedComponentRender($name, $props, $context, $hostTemplateName, $index); } catch (\Throwable $e) { $this->throwRuntimeError($name, $e); } diff --git a/src/TwigComponent/src/Twig/ComponentNode.php b/src/TwigComponent/src/Twig/ComponentNode.php index b356de8138a..2c334eacb6c 100644 --- a/src/TwigComponent/src/Twig/ComponentNode.php +++ b/src/TwigComponent/src/Twig/ComponentNode.php @@ -11,9 +11,10 @@ namespace Symfony\UX\TwigComponent\Twig; +use Symfony\UX\TwigComponent\BlockStack; use Twig\Compiler; -use Twig\Node\EmbedNode; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Node; /** * @author Fabien Potencier @@ -21,12 +22,20 @@ * * @internal */ -final class ComponentNode extends EmbedNode +final class ComponentNode extends Node { - public function __construct(string $component, string $template, int $index, AbstractExpression $variables, bool $only, int $lineno, string $tag) + public function __construct(string $component, string $embeddedTemplateName, int $embeddedTemplateIndex, ?AbstractExpression $props, bool $only, int $lineno, string $tag) { - parent::__construct($template, $index, $variables, $only, false, $lineno, $tag); + $nodes = []; + if (null !== $props) { + $nodes['props'] = $props; + } + parent::__construct($nodes, [], $lineno, $tag); + + $this->setAttribute('only', $only); + $this->setAttribute('embedded_template', $embeddedTemplateName); + $this->setAttribute('embedded_index', $embeddedTemplateIndex); $this->setAttribute('component', $component); } @@ -34,14 +43,21 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); + /* + * Block 1) PreCreateForRender handling + * + * We call code to trigger the PreCreateForRender event. If the event returns + * a string, we return that string and skip the rest of the rendering process. + */ $compiler ->write('$preRendered = $this->extensions[') ->string(ComponentExtension::class) - ->raw(']->preRender(') + ->raw(']->extensionPreCreateForRender(') ->string($this->getAttribute('component')) ->raw(', ') ->raw('twig_to_array(') - ->subcompile($this->getNode('variables')) + ; + $this->writeProps($compiler) ->raw(')') ->raw(");\n") ; @@ -58,32 +74,85 @@ public function compile(Compiler $compiler): void ->indent() ; + /* + * Block 2) Create the component & return render info + * + * We call code that creates the component and dispatches the + * PreRender event. The result $preRenderEvent variable holds + * the final template, template index & variables. + */ $compiler - ->write('$embeddedContext = $this->extensions[') + ->write('$preRenderEvent = $this->extensions[') ->string(ComponentExtension::class) - ->raw(']->embeddedContext(') + ->raw(']->startEmbeddedComponentRender(') ->string($this->getAttribute('component')) ->raw(', twig_to_array(') - ->subcompile($this->getNode('variables')) + ; + $this->writeProps($compiler) ->raw('), ') ->raw($this->getAttribute('only') ? '[]' : '$context') ->raw(', ') - ->string(TemplateNameParser::parse($this->getAttribute('name'))) + ->string(TemplateNameParser::parse($this->getAttribute('embedded_template'))) ->raw(', ') - ->raw($this->getAttribute('index')) + ->raw($this->getAttribute('embedded_index')) ->raw(");\n") ; + $compiler + ->write('$embeddedContext = $preRenderEvent->getVariables();') + ->raw("\n") + // Add __parent__ to the embedded context: this is used in its extends + // Note: PreRenderEvent::getTemplateIndex() is not used here. This is + // only used during "normal" {{ component() }} rendering, which allows + // you to target rendering a specific "embedded template" that originally + // came from a {% component %} tag. This is used by LiveComponents to + // allow an "embedded component" syntax live component to be re-rendered. + // In this case, we are obviously rendering an entire template, which + // happens to contain a {% component %} tag. So we don't need to worry + // about trying to allow a specific embedded template to be targeted. + ->write('$embeddedContext["__parent__"] = $preRenderEvent->getTemplate();') + ->raw("\n") + ; + + /* + * Block 3) Add & update the block stack + * + * We add the outerBlock to the context if it doesn't exist yet. + * Then add them to the block stack and get the converted embedded blocks. + */ + $compiler->write('if (!isset($embeddedContext["outerBlocks"])) {') + ->raw("\n") + ->indent() + ->write(sprintf('$embeddedContext["outerBlocks"] = new \%s();', BlockStack::class)) + ->raw("\n") + ->outdent() + ->write('}') + ->raw("\n"); $compiler->write('$embeddedBlocks = $embeddedContext[') ->string('outerBlocks') ->raw(']->convert($blocks, ') - ->raw($this->getAttribute('index')) + ->raw($this->getAttribute('embedded_index')) ->raw(");\n") ; - $this->addGetTemplate($compiler); - $compiler->raw('->display($embeddedContext, $embeddedBlocks);'); - $compiler->raw("\n"); + /* + * Block 4) Render the component template + * + * This will actually render the child component template. + */ + $compiler + ->write('$this->loadTemplate(') + ->string($this->getAttribute('embedded_template')) + ->raw(', ') + ->repr($this->getTemplateName()) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(', ') + ->string($this->getAttribute('embedded_index')) + ->raw(')') + ->raw('->display($embeddedContext, $embeddedBlocks);') + ->raw("\n") + ; $compiler->write('$this->extensions[') ->string(ComponentExtension::class) @@ -97,4 +166,13 @@ public function compile(Compiler $compiler): void ->raw("\n") ; } + + private function writeProps(Compiler $compiler): Compiler + { + if ($this->hasNode('props')) { + return $compiler->subcompile($this->getNode('props')); + } + + return $compiler->raw('[]'); + } } diff --git a/src/TwigComponent/src/Twig/ComponentTokenParser.php b/src/TwigComponent/src/Twig/ComponentTokenParser.php index 1f98e90b0cd..8c5151bbb0c 100644 --- a/src/TwigComponent/src/Twig/ComponentTokenParser.php +++ b/src/TwigComponent/src/Twig/ComponentTokenParser.php @@ -12,7 +12,6 @@ namespace Symfony\UX\TwigComponent\Twig; use Symfony\UX\TwigComponent\BlockStack; -use Symfony\UX\TwigComponent\ComponentFactory; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; @@ -29,40 +28,22 @@ */ final class ComponentTokenParser extends AbstractTokenParser { - /** @var ComponentFactory|callable():ComponentFactory */ - private $factory; - private array $lineAndFileCounts = []; - /** - * @param callable():ComponentFactory $factory - */ - public function __construct(callable $factory) - { - $this->factory = $factory; - } - public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $parent = $this->parser->getExpressionParser()->parseExpression(); - $componentName = $this->componentName($parent); - $componentMetadata = $this->factory()->metadataFor($componentName); - - [$variables, $only] = $this->parseArguments(); + $componentName = $this->componentName($this->parser->getExpressionParser()->parseExpression()); - if (null === $variables) { - $variables = new ArrayExpression([], $parent->getTemplateLine()); - } + [$propsExpression, $only] = $this->parseArguments(); - $parentToken = new Token(Token::STRING_TYPE, $componentMetadata->getTemplate(), $token->getLine()); - $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); - - // inject a fake parent to make the parent() function work + // Write a fake: "extends __parent__" into the "embedded" template. + // The `__parent__` will be passed in as a context variable. + $fakeParentToken = new Token(Token::NAME_TYPE, '__parent__', $token->getLine()); $stream->injectTokens([ new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), new Token(Token::NAME_TYPE, 'extends', $token->getLine()), - $parentToken, + $fakeParentToken, new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), // Add an empty block which can act as a fallback for when an outer @@ -77,21 +58,16 @@ public function parse(Token $token): Node new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), ]); + // create the "fake" ModuleNode template then add it to the parser $module = $this->parser->parse($stream, fn (Token $token) => $token->test("end{$this->getTag()}"), true); - - // override the parent with the correct one - if ($fakeParentToken === $parentToken) { - $module->setNode('parent', $parent); - } - $this->parser->embedTemplate($module); - // use deterministic index for the embedded template, so it can be loaded in a controlled manner + // override the embedded index with a deterministic value, so it can be loaded in a controlled manner $module->setAttribute('index', $this->generateEmbeddedTemplateIndex(TemplateNameParser::parse($stream->getSourceContext()->getName()), $token->getLine())); $stream->expect(Token::BLOCK_END_TYPE); - return new ComponentNode($componentName, $module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $token->getLine(), $this->getTag()); + return new ComponentNode($componentName, $module->getTemplateName(), $module->getAttribute('index'), $propsExpression, $only, $token->getLine(), $this->getTag()); } public function getTag(): string @@ -112,15 +88,9 @@ private function componentName(AbstractExpression $expression): string throw new \LogicException('Could not parse component name.'); } - private function factory(): ComponentFactory - { - if (\is_callable($this->factory)) { - $this->factory = ($this->factory)(); - } - - return $this->factory; - } - + /** + * @return array{ArrayExpression|null, bool} + */ private function parseArguments(): array { $stream = $this->parser->getStream(); @@ -132,7 +102,6 @@ private function parseArguments(): array } $only = false; - if ($stream->nextIf(Token::NAME_TYPE, 'only')) { $only = true; }