Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TwigComponent] Use a RuntimeExtension #2168

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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