Skip to content

Commit

Permalink
feature #2168 [TwigComponent] Use a RuntimeExtension (smnandre)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[TwigComponent] Use a RuntimeExtension

* Hide implementation & start decoupling the template from the event system
* Optimize (a bit) the ComponentNode

Commits
-------

8e99e14 [TwigComponent] Use a RuntimeExtension
  • Loading branch information
javiereguiluz committed Sep 16, 2024
2 parents 8dd706d + 8e99e14 commit 292f41d
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use Symfony\UX\TwigComponent\DependencyInjection\Compiler\TwigComponentPass;
use Symfony\UX\TwigComponent\Twig\ComponentExtension;
use Symfony\UX\TwigComponent\Twig\ComponentLexer;
use Symfony\UX\TwigComponent\Twig\ComponentRuntime;
use Symfony\UX\TwigComponent\Twig\TwigEnvironmentConfigurator;

/**
Expand Down Expand Up @@ -102,7 +103,13 @@ 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'])
;

$container->register('.ux.twig_component.twig.component_runtime', ComponentRuntime::class)
->setArguments([
new Reference('ux.twig_component.component_renderer'),
])
->addTag('twig.runtime')
;

$container->register('ux.twig_component.twig.lexer', ComponentLexer::class);
Expand Down
62 changes: 2 additions & 60 deletions src/TwigComponent/src/Twig/ComponentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@

namespace Symfony\UX\TwigComponent\Twig;

use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\UX\TwigComponent\ComponentRenderer;
use Symfony\UX\TwigComponent\CVA;
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
use Twig\DeprecatedCallableInfo;
use Twig\Error\RuntimeError;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

Expand All @@ -26,23 +21,12 @@
*
* @internal
*/
final class ComponentExtension extends AbstractExtension implements ServiceSubscriberInterface
final class ComponentExtension extends AbstractExtension
{
public function __construct(private ContainerInterface $container)
{
}

public static function getSubscribedServices(): array
{
return [
ComponentRenderer::class,
];
}

public function getFunctions(): array
{
return [
new TwigFunction('component', [$this, 'render'], ['is_safe' => ['all']]),
new TwigFunction('component', [ComponentRuntime::class, 'render'], ['is_safe' => ['all']]),
new TwigFunction('cva', [$this, 'cva'], [
...(class_exists(DeprecatedCallableInfo::class)
? ['deprecation_info' => new DeprecatedCallableInfo('symfony/ux-twig-component', '2.20', 'html_cva', 'twig/html-extra')]
Expand All @@ -59,38 +43,6 @@ public function getTokenParsers(): array
];
}

public function render(string $name, array $props = []): string
{
try {
return $this->container->get(ComponentRenderer::class)->createAndRender($name, $props);
} catch (\Throwable $e) {
$this->throwRuntimeError($name, $e);
}
}

public function extensionPreCreateForRender(string $name, array $props): ?string
{
try {
return $this->container->get(ComponentRenderer::class)->preCreateForRender($name, $props);
} catch (\Throwable $e) {
$this->throwRuntimeError($name, $e);
}
}

public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent
{
try {
return $this->container->get(ComponentRenderer::class)->startEmbeddedComponentRender($name, $props, $context, $hostTemplateName, $index);
} catch (\Throwable $e) {
$this->throwRuntimeError($name, $e);
}
}

public function finishEmbeddedComponentRender(): void
{
$this->container->get(ComponentRenderer::class)->finishEmbeddedComponentRender();
}

/**
* Create a CVA instance.
*
Expand Down Expand Up @@ -119,14 +71,4 @@ public function cva(array $cva): CVA
$cva['defaultVariants'] ?? [],
);
}

private function throwRuntimeError(string $name, \Throwable $e): void
{
// if it's already a Twig RuntimeError, just rethrow it
if ($e instanceof RuntimeError) {
throw $e;
}

throw new RuntimeError(\sprintf('Error rendering "%s" component: "%s"', $name, $e->getMessage()), previous: $e);
}
}
44 changes: 20 additions & 24 deletions src/TwigComponent/src/Twig/ComponentNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,30 @@ public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);

$useYield = method_exists(Environment::class, 'useYield') && $compiler->getEnvironment()->useYield();

// since twig/twig 3.9.0: Using the internal "twig_to_array" function is deprecated.
if (method_exists(CoreExtension::class, 'toArray')) {
$twig_to_array = 'Twig\Extension\CoreExtension::toArray';
} else {
$twig_to_array = 'twig_to_array';
}

$componentRuntime = $compiler->getVarName();

$compiler
->write(\sprintf('$%s = $this->env->getRuntime(', $componentRuntime))
->string(ComponentRuntime::class)
->raw(");\n");

/*
* 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(']->extensionPreCreateForRender(')
->write(\sprintf('$preRendered = $%s->preRender(', $componentRuntime))
->string($this->getAttribute('component'))
->raw(', ')
->raw($twig_to_array)
Expand Down Expand Up @@ -96,9 +103,7 @@ public function compile(Compiler $compiler): void
* the final template, template index & variables.
*/
$compiler
->write('$preRenderEvent = $this->extensions[')
->string(ComponentExtension::class)
->raw(']->startEmbeddedComponentRender(')
->write(\sprintf('$preRenderEvent = $%s->startEmbedComponent(', $componentRuntime))
->string($this->getAttribute('component'))
->raw(', ')
->raw($twig_to_array)
Expand All @@ -111,6 +116,7 @@ public function compile(Compiler $compiler): void
->raw(', ')
->raw($this->getAttribute('embedded_index'))
->raw(");\n");

$compiler
->write('$embeddedContext = $preRenderEvent->getVariables();')
->raw("\n")
Expand All @@ -132,18 +138,11 @@ public function compile(Compiler $compiler): void
* 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('}')
$compiler
->write(\sprintf('$embeddedContext["outerBlocks"] ??= new \%s();', BlockStack::class))
->raw("\n");

$compiler->write('$embeddedBlocks = $embeddedContext[')
->string('outerBlocks')
->raw(']->convert($blocks, ')
$compiler->write('$embeddedBlocks = $embeddedContext["outerBlocks"]->convert($blocks, ')
->raw($this->getAttribute('embedded_index'))
->raw(");\n");

Expand All @@ -152,9 +151,8 @@ public function compile(Compiler $compiler): void
*
* This will actually render the child component template.
*/
if (method_exists(Environment::class, 'useYield') && $compiler->getEnvironment()->useYield()) {
$compiler
->write('yield from ');
if ($useYield) {
$compiler->write('yield from ');
}
$compiler
->write('$this->loadTemplate(')
Expand All @@ -167,7 +165,7 @@ public function compile(Compiler $compiler): void
->string($this->getAttribute('embedded_index'))
->raw(')');

if (method_exists(Environment::class, 'useYield') && $compiler->getEnvironment()->useYield()) {
if ($useYield) {
$compiler->raw('->unwrap()->yield(');
} else {
$compiler->raw('->display(');
Expand All @@ -176,10 +174,8 @@ public function compile(Compiler $compiler): void
->raw('$embeddedContext, $embeddedBlocks')
->raw(");\n");

$compiler->write('$this->extensions[')
->string(ComponentExtension::class)
->raw(']->finishEmbeddedComponentRender()')
->raw(";\n")
$compiler->write(\sprintf('$%s->finishEmbedComponent();', $componentRuntime))
->raw("\n")
;

$compiler
Expand Down
59 changes: 59 additions & 0 deletions src/TwigComponent/src/Twig/ComponentRuntime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\TwigComponent\Twig;

use Symfony\UX\TwigComponent\ComponentRenderer;
use Symfony\UX\TwigComponent\Event\PreRenderEvent;

/**
* @author Kevin Bond <kevinbond@gmail.com>
* @author Simon André <smn.andre@gmail.com>
*
* @internal
*/
final class ComponentRuntime
{
public function __construct(
private readonly ComponentRenderer $renderer,
) {
}

/**
* @param array<string, mixed> $props
*/
public function render(string $name, array $props = []): string
{
return $this->renderer->createAndRender($name, $props);
}

/**
* @param array<string, mixed> $props
*/
public function preRender(string $name, array $props): ?string
{
return $this->renderer->preCreateForRender($name, $props);
}

/**
* @param array<string, mixed> $props
* @param array<string, mixed> $context
*/
public function startEmbedComponent(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent
{
return $this->renderer->startEmbeddedComponentRender($name, $props, $context, $hostTemplateName, $index);
}

public function finishEmbedComponent(): void
{
$this->renderer->finishEmbeddedComponentRender();
}
}
2 changes: 1 addition & 1 deletion src/TwigComponent/src/Twig/PropsTokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function parse(Token $token): Node
}
}

return new PropsNode($names, $values, $token->getLine(), $token->getValue());
return new PropsNode($names, $values, $token->getLine());
}

public function getTag(): string
Expand Down
1 change: 1 addition & 0 deletions src/TwigComponent/src/Twig/TwigEnvironmentConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function __construct(
public function configure(Environment $environment): void
{
$this->decorated->configure($environment);

$environment->setLexer(new ComponentLexer($environment));

if (class_exists(EscaperRuntime::class)) {
Expand Down

0 comments on commit 292f41d

Please sign in to comment.