Skip to content

Commit de8ce35

Browse files
committed
add ux:icons:warm-cache command
1 parent dbd1716 commit de8ce35

File tree

10 files changed

+213
-18
lines changed

10 files changed

+213
-18
lines changed

src/Icons/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ site.
5757

5858
To avoid having to parse icon files on every request, icons are cached.
5959

60+
In production, you can pre-warm the cache by running the following command:
61+
62+
```bash
63+
bin/console ux:icons:warm-cache
64+
```
65+
66+
This command looks in all your twig templates for `ux_icon` calls and caches the icons it finds.
67+
6068
> [!NOTE]
6169
> During development, if you change an icon, you will need to clear the cache (`bin/console cache:clear`)
6270
> to see the changes.

src/Icons/config/services.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Symfony\UX\Icons\Command\WarmCacheCommand;
1415
use Symfony\UX\Icons\IconRenderer;
1516
use Symfony\UX\Icons\Registry\CacheIconRegistry;
1617
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
18+
use Symfony\UX\Icons\Twig\IconFinder;
1719
use Symfony\UX\Icons\Twig\UXIconComponent;
1820
use Symfony\UX\Icons\Twig\UXIconComponentListener;
1921
use Symfony\UX\Icons\Twig\UXIconExtension;
@@ -53,5 +55,17 @@
5355

5456
->set('.ux_icons.twig_component.icon', UXIconComponent::class)
5557
->tag('twig.component', ['key' => 'UX:Icon'])
58+
59+
->set('.ux_icons.twig_icon_finder', IconFinder::class)
60+
->args([
61+
service('twig'),
62+
])
63+
64+
->set('.ux_icons.command.warm_cache', WarmCacheCommand::class)
65+
->args([
66+
service('.ux_icons.cache_icon_registry'),
67+
service('.ux_icons.twig_icon_finder'),
68+
])
69+
->tag('console.command')
5670
;
5771
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Icons\Command;
13+
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
use Symfony\UX\Icons\Exception\IconNotFoundException;
20+
use Symfony\UX\Icons\Registry\CacheIconRegistry;
21+
use Symfony\UX\Icons\Twig\IconFinder;
22+
23+
/**
24+
* @author Kevin Bond <kevinbond@gmail.com>
25+
*
26+
* @internal
27+
*/
28+
#[AsCommand(
29+
name: 'ux:icons:warm-cache',
30+
description: 'Warm the icon cache',
31+
)]
32+
final class WarmCacheCommand extends Command
33+
{
34+
public function __construct(private CacheIconRegistry $registry, private IconFinder $icons)
35+
{
36+
parent::__construct();
37+
}
38+
39+
protected function execute(InputInterface $input, OutputInterface $output): int
40+
{
41+
$io = new SymfonyStyle($input, $output);
42+
$io->comment('Warming the icon cache...');
43+
44+
foreach ($this->icons->icons() as $icon) {
45+
try {
46+
$this->registry->get($icon, refresh: true);
47+
48+
if ($output->isVerbose()) {
49+
$io->writeln(sprintf(' Warmed icon <comment>%s</comment>.', $icon));
50+
}
51+
} catch (IconNotFoundException) {
52+
}
53+
}
54+
55+
$io->success('Icon cache warmed.');
56+
57+
return Command::SUCCESS;
58+
}
59+
}

src/Icons/src/Registry/CacheIconRegistry.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ public function __construct(private IconRegistryInterface $inner, private CacheI
2727
{
2828
}
2929

30-
public function get(string $name): Icon
31-
{
32-
return $this->fetchIcon($name);
33-
}
34-
35-
private function fetchIcon(string $name, bool $refresh = false): Icon
30+
public function get(string $name, bool $refresh = false): Icon
3631
{
3732
if (!Icon::isValidName($name)) {
3833
throw new IconNotFoundException(sprintf('The icon name "%s" is not valid.', $name));

src/Icons/src/Twig/IconFinder.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Icons\Twig;
13+
14+
use Symfony\Component\Finder\Finder;
15+
use Twig\Environment;
16+
use Twig\Loader\ChainLoader;
17+
use Twig\Loader\FilesystemLoader;
18+
use Twig\Loader\LoaderInterface;
19+
20+
/**
21+
* @author Kevin Bond <kevinbond@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
final class IconFinder
26+
{
27+
public function __construct(private Environment $twig)
28+
{
29+
}
30+
31+
/**
32+
* @return string[]
33+
*/
34+
public function icons(): array
35+
{
36+
$found = [];
37+
38+
foreach ($this->files($this->twig->getLoader()) as $file) {
39+
$contents = file_get_contents($file);
40+
41+
if (preg_match_all('#ux_icon\(["\']([\w:-]+)["\']#', $contents, $matches)) {
42+
$found[] = $matches[1];
43+
}
44+
45+
if (preg_match_all('#name=["\']([\w:-]+)["\']#', $contents, $matches)) {
46+
$found[] = $matches[1];
47+
}
48+
}
49+
50+
return array_unique(array_merge(...$found));
51+
}
52+
53+
/**
54+
* @return string[]
55+
*/
56+
private function files(LoaderInterface $loader): iterable
57+
{
58+
$files = [];
59+
60+
if ($loader instanceof FilesystemLoader) {
61+
foreach ($loader->getNamespaces() as $namespace) {
62+
foreach ($loader->getPaths($namespace) as $path) {
63+
foreach ((new Finder())->files()->in($path)->name('*.twig') as $file) {
64+
$file = (string) $file;
65+
if (!\in_array($file, $files, true)) {
66+
yield $file;
67+
}
68+
69+
$files[] = $file;
70+
}
71+
}
72+
}
73+
}
74+
75+
if ($loader instanceof ChainLoader) {
76+
foreach ($loader->getLoaders() as $subLoader) {
77+
yield from $this->files($subLoader);
78+
}
79+
}
80+
}
81+
}

src/Icons/tests/Fixtures/TestKernel.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ protected function configureContainer(ContainerConfigurator $c): void
3939
'http_client' => true,
4040
]);
4141

42+
$c->extension('twig', [
43+
'default_path' => __DIR__.'/templates',
44+
]);
45+
4246
$c->extension('twig_component', [
4347
'defaults' => [],
4448
'anonymous_template_directory' => 'components',
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<ul class="svg">
2+
<li id="first">{{ ux_icon('user', {class: 'h-8 w-8'}) }}</li>
3+
<li id="second">{{ ux_icon('user') }}</li>
4+
<li id="third">{{ ux_icon('sub:check', {'data-action': 'string "with" quotes'}) }}</li>
5+
<li id="fifth"><twig:UX:Icon name="user" class="h-8 w-8" /></li>
6+
<li id="sixth"><twig:UX:Icon name="sub:check" /></li>
7+
<li id="seventh"><twig:UX:Icon :name="'sub:'~'check'" /></li>
8+
</ul>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{ ux_icon('something:invalid') }}
2+
{{ ux_icon('invalid') }}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Icons\Tests\Integration\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Zenstruck\Console\Test\InteractsWithConsole;
16+
17+
/**
18+
* @author Kevin Bond <kevinbond@gmail.com>
19+
*/
20+
final class WarmCacheCommandTest extends KernelTestCase
21+
{
22+
use InteractsWithConsole;
23+
24+
public function testCanWarmCache(): void
25+
{
26+
$this->executeConsoleCommand('ux:icons:warm-cache -v')
27+
->assertSuccessful()
28+
->assertOutputContains('Warming the icon cache...')
29+
->assertOutputContains('Warmed icon user.')
30+
->assertOutputContains('Warmed icon sub:check.')
31+
->assertOutputContains('Icon cache warmed.')
32+
;
33+
}
34+
}

src/Icons/tests/Integration/RenderIconsInTwigTest.php

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,7 @@ final class RenderIconsInTwigTest extends KernelTestCase
2121
{
2222
public function testRenderIcons(): void
2323
{
24-
$output = self::getContainer()->get(Environment::class)->createTemplate(<<<TWIG
25-
<ul class="svg">
26-
<li id="first">{{ ux_icon('user', {class: 'h-8 w-8'}) }}</li>
27-
<li id="second">{{ ux_icon('user') }}</li>
28-
<li id="third">{{ ux_icon('sub:check', {'data-action': 'string "with" quotes'}) }}</li>
29-
<li id="fifth"><twig:UX:Icon name="user" class="h-8 w-8" /></li>
30-
<li id="sixth"><twig:UX:Icon name="sub:check" /></li>
31-
<li id="seventh"><twig:UX:Icon :name="'sub:'~'check'" /></li>
32-
</ul>
33-
TWIG
34-
)->render();
24+
$output = self::getContainer()->get(Environment::class)->render('template1.html.twig');
3525

3626
$this->assertSame(<<<HTML
3727
<ul class="svg">
@@ -43,7 +33,7 @@ public function testRenderIcons(): void
4333
<li id="seventh"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd"></path></svg></li>
4434
</ul>
4535
HTML,
46-
preg_replace("#(\s+)</li>#m", '</li>', $output)
36+
trim($output)
4737
);
4838
}
4939
}

0 commit comments

Comments
 (0)