Skip to content

Commit f327a5f

Browse files
committed
add ux:icons:import command
1 parent e2a3e24 commit f327a5f

File tree

9 files changed

+269
-0
lines changed

9 files changed

+269
-0
lines changed

src/Icons/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ composer require symfony/ux-icons
1111
No icons are provided by this package. Add your svg icons to the `assets/icons/` directory and commit them.
1212
The name of the file is used as the name of the icon (`name.svg` will be named `name`).
1313

14+
### Import Command
15+
16+
The [Iconify Design](https://iconify.design/) has a huge searchable repository of icons from
17+
many different icon sets. This package provides a command to locally install icons from this
18+
site.
19+
20+
1. Visit [Iconify Design](https://icon-sets.iconify.design/) and search for an icon
21+
you'd like to use. Once you find one you like, visit the icon's profile page and use the widget
22+
to copy its name. For instance, https://icon-sets.iconify.design/flowbite/user-solid/ has the name
23+
`flowbite:user-solid`.
24+
2. Run the following command, replacing `flowbite:user-solid` with the name of the icon you'd like
25+
to install:
26+
27+
```bash
28+
bin/console ux:icons:import flowbite:user-solid # saved as `user-solid.svg` and name is `user-solid`
29+
30+
# adjust the local name
31+
bin/console ux:icons:import flowbite:user-solid@user # saved as `user.svg` and name is `user`
32+
33+
# import several at a time
34+
bin/console ux:icons:import flowbite:user-solid flowbite:home-solid
35+
```
36+
1437
## Usage
1538

1639
```twig

src/Icons/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"require-dev": {
3535
"symfony/console": "^6.4|^7.0",
36+
"symfony/http-client": "6.4|^7.0",
3637
"symfony/phpunit-bridge": "^6.3|^7.0",
3738
"symfony/ux-twig-component": "^2.14",
3839
"zenstruck/console-test": "^1.5"

src/Icons/config/services.php

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

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

14+
use Symfony\UX\Icons\Command\ImportIconCommand;
15+
use Symfony\UX\Icons\Iconify;
1416
use Symfony\UX\Icons\IconRenderer;
1517
use Symfony\UX\Icons\Registry\CacheIconRegistry;
1618
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
@@ -54,5 +56,17 @@
5456

5557
->set('.ux_icons.twig_component.icon', UXIconComponent::class)
5658
->tag('twig.component', ['key' => 'UX:Icon'])
59+
60+
->set('.ux_icons.iconify', Iconify::class)
61+
->args([
62+
service('http_client')->nullOnInvalid(),
63+
])
64+
65+
->set('.ux_icons.command.import', ImportIconCommand::class)
66+
->args([
67+
service('.ux_icons.iconify'),
68+
service('.ux_icons.local_svg_icon_registry'),
69+
])
70+
->tag('console.command')
5771
;
5872
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
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+
24+
/**
25+
* @author Kevin Bond <[email protected]>
26+
*
27+
* @internal
28+
*/
29+
#[AsCommand(
30+
name: 'ux:icons:import',
31+
description: 'Import icon(s) from iconify.design',
32+
)]
33+
final class ImportIconCommand extends Command
34+
{
35+
public function __construct(private Iconify $iconify, private LocalSvgIconRegistry $registry)
36+
{
37+
parent::__construct();
38+
}
39+
40+
protected function configure(): void
41+
{
42+
$this
43+
->addArgument(
44+
'names',
45+
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
46+
'Icon name from iconify.design (suffix with "@<name>" to rename locally)',
47+
)
48+
;
49+
}
50+
51+
protected function execute(InputInterface $input, OutputInterface $output): int
52+
{
53+
$io = new SymfonyStyle($input, $output);
54+
$names = $input->getArgument('names');
55+
$result = Command::SUCCESS;
56+
57+
foreach ($names as $name) {
58+
if (!preg_match('#^(([\w-]+):([\w-]+))(@([\w-]+))?$#', $name, $matches)) {
59+
$io->error(sprintf('Invalid icon name "%s".', $name));
60+
$result = Command::FAILURE;
61+
62+
continue;
63+
}
64+
65+
[,,$prefix, $name] = $matches;
66+
$localName = $matches[5] ?? $name;
67+
68+
$io->comment(sprintf('Importing <info>%s:%s</info> as <info>%s</info>...', $prefix, $name, $localName));
69+
70+
try {
71+
$svg = $this->iconify->fetchSvg($prefix, $name);
72+
} catch (IconNotFoundException $e) {
73+
$io->error($e->getMessage());
74+
$result = Command::FAILURE;
75+
76+
continue;
77+
}
78+
79+
$this->registry->add($localName, $svg);
80+
81+
$io->text(sprintf("<info>Imported Icon</info>, render with <comment>{{ ux_icon('%s') }}</comment>.", $localName));
82+
$io->newLine();
83+
}
84+
85+
return $result;
86+
}
87+
}

src/Icons/src/DependencyInjection/UXIconsExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,9 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
6565
$container->getDefinition('.ux_icons.icon_renderer')
6666
->setArgument(1, $mergedConfig['default_icon_attributes'])
6767
;
68+
69+
if (!$container->getParameter('kernel.debug')) {
70+
$container->removeDefinition('.ux_icons.command.import');
71+
}
6872
}
6973
}

src/Icons/src/Iconify.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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;
13+
14+
use Symfony\Contracts\HttpClient\HttpClientInterface;
15+
use Symfony\UX\Icons\Exception\IconNotFoundException;
16+
17+
/**
18+
* @author Kevin Bond <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
final class Iconify
23+
{
24+
public function __construct(private ?HttpClientInterface $httpClient = null)
25+
{
26+
}
27+
28+
public function fetchSvg(string $prefix, string $name): string
29+
{
30+
$content = $this->http()
31+
->request('GET', sprintf('https://api.iconify.design/%s/%s.svg', $prefix, $name))
32+
->getContent()
33+
;
34+
35+
if (!str_starts_with($content, '<svg')) {
36+
throw new IconNotFoundException(sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
37+
}
38+
39+
return $content;
40+
}
41+
42+
private function http(): HttpClientInterface
43+
{
44+
return $this->httpClient ?? throw new \LogicException('You must install "symfony/http-client" to import icons. Try running "composer require symfony/http-client".');
45+
}
46+
}

src/Icons/src/Registry/LocalSvgIconRegistry.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\UX\Icons\Registry;
1313

14+
use Symfony\Component\Filesystem\Filesystem;
1415
use Symfony\Component\Finder\Finder;
1516
use Symfony\UX\Icons\Exception\IconNotFoundException;
1617
use Symfony\UX\Icons\IconRegistryInterface;
@@ -75,6 +76,13 @@ public function get(string $name): Icon
7576
return new Icon($innerSvg, $attributes);
7677
}
7778

79+
public function add(string $name, string $svg): void
80+
{
81+
$filename = sprintf('%s/%s.svg', $this->iconDir, $name);
82+
83+
(new Filesystem())->dumpFile($filename, $svg);
84+
}
85+
7886
public function getIterator(): \Traversable
7987
{
8088
foreach ($this->finder()->sortByName() as $file) {

src/Icons/tests/Fixtures/TestKernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ protected function configureContainer(ContainerConfigurator $c): void
3636
'http_method_override' => false,
3737
'php_errors' => ['log' => true],
3838
'property_access' => true,
39+
'http_client' => true,
3940
]);
4041

4142
$c->extension('twig_component', [
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 ImportIconCommandTest extends KernelTestCase
22+
{
23+
use InteractsWithConsole;
24+
25+
private const ICON_DIR = __DIR__.'/../../Fixtures/icons';
26+
private const ICONS = ['dashboard.svg', 'renamed.svg'];
27+
28+
/**
29+
* @before
30+
*
31+
* @after
32+
*/
33+
public static function cleanup(): void
34+
{
35+
$fs = new Filesystem();
36+
37+
foreach (self::ICONS as $icon) {
38+
$fs->remove(self::ICON_DIR.'/'.$icon);
39+
}
40+
}
41+
42+
public function testCanImportIcon(): void
43+
{
44+
$this->assertFileDoesNotExist($expectedFile = self::ICON_DIR.'/dashboard.svg');
45+
46+
$this->executeConsoleCommand('ux:icons:import uiw:dashboard')
47+
->assertSuccessful()
48+
->assertOutputContains('Importing uiw:dashboard as dashboard')
49+
->assertOutputContains("render with {{ ux_icon('dashboard') }}")
50+
;
51+
52+
$this->assertFileExists($expectedFile);
53+
}
54+
55+
public function testCanImportIconAndRename(): void
56+
{
57+
$this->assertFileDoesNotExist($expectedFile = self::ICON_DIR.'/renamed.svg');
58+
59+
$this->executeConsoleCommand('ux:icons:import uiw:dashboard@renamed')
60+
->assertSuccessful()
61+
->assertOutputContains('Importing uiw:dashboard as renamed')
62+
->assertOutputContains("render with {{ ux_icon('renamed') }}")
63+
;
64+
65+
$this->assertFileExists($expectedFile);
66+
}
67+
68+
public function testImportInvalidIconName(): void
69+
{
70+
$this->executeConsoleCommand('ux:icons:import something')
71+
->assertStatusCode(1)
72+
->assertOutputContains('[ERROR] Invalid icon name "something".')
73+
;
74+
}
75+
76+
public function testImportNonExistentIcon(): void
77+
{
78+
$this->executeConsoleCommand('ux:icons:import something:invalid')
79+
->assertStatusCode(1)
80+
->assertOutputContains('[ERROR] The icon "something:invalid" does not exist on iconify.design.')
81+
;
82+
83+
$this->assertFileDoesNotExist(self::ICON_DIR.'/invalid.svg');
84+
}
85+
}

0 commit comments

Comments
 (0)