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('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('"icon"', $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()); + } +}