Skip to content

Commit e9faf78

Browse files
committed
feature #1660 feat(icons): add ux:icons:lock command to "mass import" used icons (kbond)
This PR was merged into the 2.x branch. Discussion ---------- feat(icons): add `ux:icons:lock` command to "mass import" used icons | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | n/a | License | MIT There has been valid concern over using the on-demand system in production. Icons could change/be removed or the iconify service could be down. This new command searches your twig files and imports valid iconify.design icons in your twig file and mass imports them locally. By default, they aren't overwritten but using the `--force` option enables this. I think, the official best practice should be: - on-demand enabled in development only - part of your workflow includes running `ux:icons:lock` (could be automated) - (future) `ux:icons:lint` to be used in your CI to ensure all icons are available locally This could/should be reflected in the official recipe (TODO). Commits ------- 23dc48a feat(icons): add `ux:icons:lock` command to "mass import" used icons
2 parents e950156 + 23dc48a commit e9faf78

File tree

6 files changed

+226
-3
lines changed

6 files changed

+226
-3
lines changed

src/Icons/config/iconify.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\UX\Icons\Command\ImportIconCommand;
15+
use Symfony\UX\Icons\Command\LockIconsCommand;
1516
use Symfony\UX\Icons\Iconify;
1617
use Symfony\UX\Icons\Registry\IconifyOnDemandRegistry;
1718

@@ -36,5 +37,13 @@
3637
service('.ux_icons.local_svg_icon_registry'),
3738
])
3839
->tag('console.command')
40+
41+
->set('.ux_icons.command.lock', LockIconsCommand::class)
42+
->args([
43+
service('.ux_icons.iconify'),
44+
service('.ux_icons.local_svg_icon_registry'),
45+
service('.ux_icons.icon_finder'),
46+
])
47+
->tag('console.command')
3948
;
4049
};

src/Icons/doc/index.rst

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ to fetch the icon and always use the *latest version* of the icon. It's possible
216216
that the icon could change or be removed in the future. Additionally, the cache
217217
warming process will take significantly longer if using many *on-demand* icons.
218218

219-
That's why this package provices a command to download the open source icons into
219+
That's why this package provides a command to download the open source icons into
220220
the ``assets/icons/`` directory. You can think of importing an icon as *locking it*
221221
(similar to how ``composer.lock`` *locks* your dependencies):
222222

@@ -233,6 +233,23 @@ the ``assets/icons/`` directory. You can think of importing an icon as *locking
233233

234234
Imported icons must be committed to your repository.
235235

236+
Locking On-Demand Icons
237+
~~~~~~~~~~~~~~~~~~~~~~~
238+
239+
You can *lock* (import) all the `*on-demand* <Icons On-Demand>`_ icons you're using in your project by
240+
running the following command:
241+
242+
.. code-block:: terminal
243+
244+
$ php bin/console ux:icons:lock
245+
246+
This command only imports icons that do not already exist locally. You can force
247+
the report to overwrite existing icons by using the ``--force`` option:
248+
249+
.. code-block:: terminal
250+
251+
$ php bin/console ux:icons:lock --force
252+
236253
Rendering Icons
237254
---------------
238255

@@ -369,7 +386,6 @@ Performance
369386
The UX Icons component is designed to be fast. The following are some of
370387
the optimizations made to ensure the best performance possible.
371388

372-
373389
Caching
374390
-------
375391

src/Icons/src/Command/ImportIconCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected function configure(): void
4444
->addArgument(
4545
'names',
4646
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
47-
'Icon name from iconify.design (suffix with "@<name>" to rename locally)',
47+
'Icon name from ux.symfony.com/icons (e.g. "mdi:home")',
4848
)
4949
;
5050
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Input\InputOption;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Console\Style\SymfonyStyle;
20+
use Symfony\UX\Icons\Exception\IconNotFoundException;
21+
use Symfony\UX\Icons\Iconify;
22+
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
23+
use Symfony\UX\Icons\Twig\IconFinder;
24+
25+
/**
26+
* @author Kevin Bond <[email protected]>
27+
*
28+
* @internal
29+
*/
30+
#[AsCommand(
31+
name: 'ux:icons:lock',
32+
description: 'Scan project and import icon(s) from iconify.design',
33+
)]
34+
final class LockIconsCommand extends Command
35+
{
36+
public function __construct(
37+
private Iconify $iconify,
38+
private LocalSvgIconRegistry $registry,
39+
private IconFinder $iconFinder,
40+
) {
41+
parent::__construct();
42+
}
43+
44+
protected function configure(): void
45+
{
46+
$this
47+
->addOption(
48+
name: 'force',
49+
mode: InputOption::VALUE_NONE,
50+
description: 'Force re-import of all found icons'
51+
)
52+
;
53+
}
54+
55+
protected function execute(InputInterface $input, OutputInterface $output): int
56+
{
57+
$io = new SymfonyStyle($input, $output);
58+
$force = $input->getOption('force');
59+
$count = 0;
60+
61+
$io->comment('Scanning project for icons...');
62+
63+
foreach ($this->iconFinder->icons() as $icon) {
64+
if (2 !== \count($parts = explode(':', $icon))) {
65+
continue;
66+
}
67+
68+
if (!$force && $this->registry->has($icon)) {
69+
// icon already imported
70+
continue;
71+
}
72+
73+
[$prefix, $name] = $parts;
74+
75+
try {
76+
$svg = $this->iconify->fetchSvg($prefix, $name);
77+
} catch (IconNotFoundException) {
78+
// icon not found on iconify
79+
continue;
80+
}
81+
82+
$this->registry->add(sprintf('%s/%s', $prefix, $name), $svg);
83+
84+
$license = $this->iconify->metadataFor($prefix)['license'];
85+
++$count;
86+
87+
$io->text(sprintf(
88+
" <fg=bright-green;options=bold>✓</> Imported <fg=bright-white;bg=black>%s:</><fg=bright-magenta;bg=black;options>%s</> (License: <href=%s>%s</>). Render with: <comment>{{ ux_icon('%s') }}</comment>",
89+
$prefix,
90+
$name,
91+
$license['url'],
92+
$license['title'],
93+
$icon,
94+
));
95+
}
96+
97+
$io->success(sprintf('Imported %d icons.', $count));
98+
99+
return Command::SUCCESS;
100+
}
101+
}

src/Icons/src/Registry/LocalSvgIconRegistry.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ public function get(string $name): Icon
3636
return Icon::fromFile($filename);
3737
}
3838

39+
public function has(string $name): bool
40+
{
41+
try {
42+
$this->get($name);
43+
44+
return true;
45+
} catch (IconNotFoundException) {
46+
return false;
47+
}
48+
}
49+
3950
public function add(string $name, string $svg): void
4051
{
4152
$filename = sprintf('%s/%s.svg', $this->iconDir, $name);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\Component\Filesystem\Filesystem;
16+
use Zenstruck\Console\Test\InteractsWithConsole;
17+
18+
/**
19+
* @author Kevin Bond <[email protected]>
20+
*/
21+
final class LockIconsCommandTest extends KernelTestCase
22+
{
23+
use InteractsWithConsole;
24+
25+
private const ICONS = [
26+
__DIR__.'/../../Fixtures/icons/iconamoon/3d-duotone.svg',
27+
__DIR__.'/../../Fixtures/icons/flag/eu-4x3.svg',
28+
];
29+
30+
/**
31+
* @before
32+
*
33+
* @after
34+
*/
35+
public static function cleanup(): void
36+
{
37+
$fs = new Filesystem();
38+
39+
foreach (self::ICONS as $icon) {
40+
$fs->remove($icon);
41+
}
42+
}
43+
44+
public function testImportFoundIcons(): void
45+
{
46+
foreach (self::ICONS as $icon) {
47+
$this->assertFileDoesNotExist($icon);
48+
}
49+
50+
$this->executeConsoleCommand('ux:icons:lock')
51+
->assertSuccessful()
52+
->assertOutputContains('Scanning project for icons...')
53+
->assertOutputContains('Imported flag:eu-4x3')
54+
->assertOutputContains('Imported iconamoon:3d-duotone')
55+
->assertOutputContains('Imported 2 icons')
56+
;
57+
58+
foreach (self::ICONS as $icon) {
59+
$this->assertFileExists($icon);
60+
}
61+
62+
$this->executeConsoleCommand('ux:icons:lock')
63+
->assertSuccessful()
64+
->assertOutputContains('Imported 0 icons')
65+
;
66+
}
67+
68+
public function testForceImportFoundIcons(): void
69+
{
70+
$this->executeConsoleCommand('ux:icons:lock')
71+
->assertSuccessful()
72+
->assertOutputContains('Scanning project for icons...')
73+
->assertOutputContains('Imported flag:eu-4x3')
74+
->assertOutputContains('Imported iconamoon:3d-duotone')
75+
->assertOutputContains('Imported 2 icons')
76+
;
77+
78+
$this->executeConsoleCommand('ux:icons:lock --force')
79+
->assertSuccessful()
80+
->assertOutputContains('Scanning project for icons...')
81+
->assertOutputContains('Imported flag:eu-4x3')
82+
->assertOutputContains('Imported iconamoon:3d-duotone')
83+
->assertOutputContains('Imported 2 icons')
84+
;
85+
}
86+
}

0 commit comments

Comments
 (0)