diff --git a/src/Icons/CHANGELOG.md b/src/Icons/CHANGELOG.md
index 8ff2aad1fb0..0e3ce1c93b8 100644
--- a/src/Icons/CHANGELOG.md
+++ b/src/Icons/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+## 2.22.0
+
+- Add support for static icons compilation during Twig warm-up (#2150).
+
## 2.20.0
- Add `aliases` configuration option to define icon alternative names.
diff --git a/src/Icons/src/Twig/UXIconExtension.php b/src/Icons/src/Twig/UXIconExtension.php
index 92ffaac24c3..4661eda284a 100644
--- a/src/Icons/src/Twig/UXIconExtension.php
+++ b/src/Icons/src/Twig/UXIconExtension.php
@@ -24,7 +24,7 @@ final class UXIconExtension extends AbstractExtension
public function getFunctions(): array
{
return [
- new TwigFunction('ux_icon', [UXIconRuntime::class, 'renderIcon'], ['is_safe' => ['html']]),
+ new TwigFunction('ux_icon', [UXIconRuntime::class, 'renderIcon'], ['node_class' => UXIconFunction::class, 'is_safe' => ['html']]),
];
}
}
diff --git a/src/Icons/src/Twig/UXIconFunction.php b/src/Icons/src/Twig/UXIconFunction.php
new file mode 100644
index 00000000000..a8af1855f77
--- /dev/null
+++ b/src/Icons/src/Twig/UXIconFunction.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Icons\Twig;
+
+use Symfony\UX\Icons\Exception\IconNotFoundException;
+use Twig\Compiler;
+use Twig\Error\RuntimeError;
+use Twig\Node\Expression\ArrayExpression;
+use Twig\Node\Expression\ConstantExpression;
+use Twig\Node\Expression\FunctionExpression;
+
+final class UXIconFunction extends FunctionExpression
+{
+ public function compile(Compiler $compiler): void
+ {
+ $arguments = $this->getNode('arguments');
+
+ $iconName = $attributes = null;
+
+ if ($arguments->hasNode('name')) {
+ $iconName = $arguments->getNode('name');
+ } elseif ($arguments->hasNode('0')) {
+ $iconName = $arguments->getNode('0');
+ }
+
+ if (!$iconName instanceof ConstantExpression) {
+ parent::compile($compiler);
+
+ return;
+ }
+
+ $iconAttributes = [];
+
+ if ($arguments->hasNode('1')) {
+ $attributes = $arguments->getNode('1');
+ } elseif ($arguments->hasNode('attributes')) {
+ $attributes = $arguments->getNode('attributes');
+ }
+
+ if ($attributes instanceof ArrayExpression) {
+ foreach ($attributes->getKeyValuePairs() as $attribute) {
+ // Cannot handle dynamic attribute values
+ if (!$attribute['key'] instanceof ConstantExpression || !$attribute['value'] instanceof ConstantExpression) {
+ parent::compile($compiler);
+
+ return;
+ }
+
+ $iconAttributes[$attribute['key']->getAttribute('value')] = $attribute['value']->getAttribute('value');
+ }
+ }
+
+ try {
+ $compiler->string(
+ $compiler
+ ->getEnvironment()
+ ->getRuntime(UXIconRuntime::class)
+ ->renderIcon($iconName->getAttribute('value'), $iconAttributes)
+ );
+ } catch (IconNotFoundException|RuntimeError) {
+ parent::compile($compiler);
+ }
+ }
+}
diff --git a/src/Icons/tests/Unit/Twig/UXIconFunctionTest.php b/src/Icons/tests/Unit/Twig/UXIconFunctionTest.php
new file mode 100644
index 00000000000..242d70eb8ca
--- /dev/null
+++ b/src/Icons/tests/Unit/Twig/UXIconFunctionTest.php
@@ -0,0 +1,118 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Icons\Tests\Unit\Twig;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\UX\Icons\Icon;
+use Symfony\UX\Icons\IconRegistryInterface;
+use Symfony\UX\Icons\IconRenderer;
+use Symfony\UX\Icons\Twig\UXIconFunction;
+use Twig\Compiler;
+use Twig\Environment;
+use Twig\Node\Expression\ArrayExpression;
+use Twig\Node\Expression\ConstantExpression;
+use Twig\Node\Expression\NameExpression;
+use Twig\Node\Node;
+use Twig\TwigFunction;
+
+final class UXIconFunctionTest extends TestCase
+{
+ public function testCompileRendersIconWithConstantNameAndAttributes(): void
+ {
+ $iconRegistry = $this->createMock(IconRegistryInterface::class);
+ $iconRenderer = new IconRenderer($iconRegistry);
+ $iconRegistry->method('get')->with('icon-name')->willReturn(new Icon(''));
+
+ $environment = $this->createMock(Environment::class);
+ $environment->method('getRuntime')->willReturn($iconRenderer);
+
+ $compiler = new Compiler($environment);
+
+ $iconName = new ConstantExpression('icon-name', 1);
+ $attributes = new ArrayExpression([
+ new ConstantExpression('class', 2),
+ new ConstantExpression('icon-class', 3),
+ ], 4);
+
+ $arguments = new Node([$iconName, $attributes]);
+
+ $node = new UXIconFunction(new TwigFunction('ux_icon', fn () => null), $arguments, 1);
+ $node->compile($compiler);
+
+ $this->assertSame('""', $compiler->getSource());
+ }
+
+ public function testCompileHandlesDynamicAttributeValues(): void
+ {
+ $iconRegistry = $this->createMock(IconRegistryInterface::class);
+ $iconRenderer = new IconRenderer($iconRegistry);
+ $iconRegistry->method('get')->with('icon-name')->willReturn(new Icon('icon'));
+
+ $environment = $this->createMock(Environment::class);
+ $environment->method('getRuntime')->willReturn($iconRenderer);
+
+ $compiler = new Compiler($environment);
+
+ $iconName = new ConstantExpression('icon-name', 1);
+ $attributes = new ArrayExpression([
+ new ConstantExpression('class', 2),
+ new NameExpression('dynamic_value', 3),
+ ], 4);
+
+ $arguments = new Node([$iconName, $attributes]);
+
+ $node = new UXIconFunction(new TwigFunction('ux_icon', fn () => null), $arguments, 1);
+ $node->compile($compiler);
+
+ $this->assertNotSame('icon', $compiler->getSource());
+ }
+
+ public function testCompileHandlesNonConstantIconName(): void
+ {
+ $iconRegistry = $this->createMock(IconRegistryInterface::class);
+ $iconRenderer = new IconRenderer($iconRegistry);
+ $iconRegistry->method('get')->with('dynamic_icon_name')->willReturn(new Icon('icon'));
+
+ $environment = $this->createMock(Environment::class);
+ $environment->method('getRuntime')->willReturn($iconRenderer);
+
+ $compiler = new Compiler($environment);
+
+ $iconName = new NameExpression('dynamic_icon_name', 1);
+ $arguments = new Node([$iconName]);
+
+ $node = new UXIconFunction(new TwigFunction('ux_icon', fn () => null), $arguments, 1);
+ $node->compile($compiler);
+
+ $this->assertNotSame('icon', $compiler->getSource());
+ }
+
+ public function testCompileWithoutAttributes(): void
+ {
+ $iconRegistry = $this->createMock(IconRegistryInterface::class);
+ $iconRenderer = new IconRenderer($iconRegistry);
+ $iconRegistry->method('get')->with('icon_name')->willReturn(new Icon('icon'));
+
+ $environment = $this->createMock(Environment::class);
+ $environment->method('getRuntime')->willReturn($iconRenderer);
+
+ $compiler = new Compiler($environment);
+
+ $iconName = new ConstantExpression('icon_name', 1);
+ $arguments = new Node([$iconName]);
+
+ $node = new UXIconFunction(new TwigFunction('ux_icon', fn () => null), $arguments, 1);
+ $node->compile($compiler);
+
+ $this->assertSame('"icon"', $compiler->getSource());
+ }
+}