Skip to content

Commit 98f1622

Browse files
committed
feature #2156 [Icons] Configure icon sets: path, alias & icon attributes (smnandre)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Icons] Configure icon sets: path, alias & icon attributes | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | Fix # | License | MIT I'm opening now if you guys have feedback, and i will add more tests the next couple days > [!NOTE] > This PR brings 3 new features: iconset aliases, custom path and attributes. So let's focus on that 😄 ```yaml ux_icons: icon_dir: '%kernel.project_dir%/assets/icons' # Default icon set default_icon_attributes: class: 'icon' fill: 'currentColor' width: '24' height: '24' icon_sets: # Local icon set (prefix mapped to a local directory) flags: path: '%kernel.project_dir%/assets/images/flags' # Remote icon set (prefix mapped to an icon set identifier) lu: alias: 'lucide' # Icon set configuration (work for local/remote) oc: icon_attributes: class: 'oc-icon' # Replace the default class stroke: 'none' # Add a new attribute fill: false # Use "false" to remove a default attribute ``` ### Default attributes ```twig <twig:ux:icon name="anything" /> {# Renders #} <svg class="icon" fill="currentColor" width="24" height="24"> ... </svg> ``` ## Icon set configuration ### Local icons: path ```twig <twig:ux:icon name="flags:fr" /> {# Renders: #} <svg class="icon" fill="currentColor" width="24" height="24"> <!-- file assets/images/flags/fr.svg --> </svg> ``` ### Remote icons: alias ```twig <twig:ux:icon name="lu:circle-check" /> {# Renders: #} <svg class="icon" fill="currentColor" width="24" height="24"> <!-- file lucide:circle-check.svg from iconify --> </svg> ``` ### Icon set attributes ```twig <twig:ux:icon name="oc:check" /> {# Renders: #} {# Attributes are merged into the default attributes #} <svg class="oc-icon" stroke="none" width="24" height="24"> <!-- file oc:check.svg from local or remote --> </svg> ``` Commits ------- 28454b8 [Icons] Configure icon sets: path, alias & icon attributes
2 parents c05aff1 + 28454b8 commit 98f1622

File tree

14 files changed

+441
-59
lines changed

14 files changed

+441
-59
lines changed

src/Icons/config/services.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\UX\Icons\Command\WarmCacheCommand;
1515
use Symfony\UX\Icons\IconCacheWarmer;
1616
use Symfony\UX\Icons\IconRenderer;
17+
use Symfony\UX\Icons\IconRendererInterface;
1718
use Symfony\UX\Icons\Registry\CacheIconRegistry;
1819
use Symfony\UX\Icons\Registry\ChainIconRegistry;
1920
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
@@ -60,7 +61,7 @@
6061
abstract_arg('icon_aliases'),
6162
])
6263

63-
->alias('Symfony\UX\Icons\IconRendererInterface', '.ux_icons.icon_renderer')
64+
->alias(IconRendererInterface::class, '.ux_icons.icon_renderer')
6465

6566
->set('.ux_icons.icon_finder', IconFinder::class)
6667
->args([

src/Icons/src/DependencyInjection/UXIconsExtension.php

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,66 @@ public function getConfigTreeBuilder(): TreeBuilder
4242
->variableNode('default_icon_attributes')
4343
->info('Default attributes to add to all icons.')
4444
->defaultValue(['fill' => 'currentColor'])
45+
->example(['class' => 'icon'])
46+
->end()
47+
->arrayNode('icon_sets')
48+
->info('Icon sets configuration.')
49+
->defaultValue([])
50+
->normalizeKeys(false)
51+
->useAttributeAsKey('prefix')
52+
->arrayPrototype()
53+
->info('the icon set prefix (e.g. "acme")')
54+
->children()
55+
->scalarNode('path')
56+
->info("The local icon set directory path.\n(cannot be used with 'alias')")
57+
->example('%kernel.project_dir%/assets/svg/acme')
58+
->end()
59+
->scalarNode('alias')
60+
->info("The remote icon set identifier.\n(cannot be used with 'path')")
61+
->example('simple-icons')
62+
->end()
63+
->arrayNode('icon_attributes')
64+
->info('Override default icon attributes for icons in this set.')
65+
->example(['class' => 'icon icon-acme', 'fill' => 'none'])
66+
->normalizeKeys(false)
67+
->variablePrototype()
68+
->end()
69+
->end()
70+
->end()
71+
->end()
72+
->validate()
73+
->ifTrue(fn (array $v) => isset($v['path']) && isset($v['alias']))
74+
->thenInvalid('You cannot define both "path" and "alias" for an icon set.')
75+
->end()
4576
->end()
4677
->arrayNode('aliases')
47-
->info('Icon aliases (alias => icon name).')
48-
->example(['dots' => 'clarity:ellipsis-horizontal-line'])
78+
->info('Icon aliases (map of alias => full name).')
79+
->example([
80+
'dots' => 'clarity:ellipsis-horizontal-line',
81+
'privacy' => 'bi:cookie',
82+
])
4983
->normalizeKeys(false)
5084
->scalarPrototype()
5185
->cannotBeEmpty()
5286
->end()
5387
->end()
5488
->arrayNode('iconify')
55-
->info('Configuration for the "on demand" icons powered by Iconify.design.')
89+
->info('Configuration for the remote icon service.')
5690
->{interface_exists(HttpClientInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
5791
->children()
5892
->booleanNode('on_demand')
59-
->info('Whether to use the "on demand" icons powered by Iconify.design.')
93+
->info('Whether to download icons "on demand".')
6094
->defaultTrue()
6195
->end()
6296
->scalarNode('endpoint')
63-
->info('The endpoint for the Iconify API.')
97+
->info('The endpoint for the Iconify icons API.')
6498
->defaultValue(Iconify::API_ENDPOINT)
6599
->cannotBeEmpty()
66100
->end()
67101
->end()
68102
->end()
69103
->booleanNode('ignore_not_found')
70-
->info('Ignore error when an icon is not found.')
104+
->info("Ignore error when an icon is not found.\nSet to 'true' to fail silently.")
71105
->defaultFalse()
72106
->end()
73107
->end()
@@ -94,9 +128,25 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
94128
$loader->load('asset_mapper.php');
95129
}
96130

131+
$iconSetAliases = [];
132+
$iconSetAttributes = [];
133+
$iconSetPaths = [];
134+
foreach ($mergedConfig['icon_sets'] as $prefix => $config) {
135+
if (isset($config['icon_attributes'])) {
136+
$iconSetAttributes[$prefix] = $config['icon_attributes'];
137+
}
138+
if (isset($config['alias'])) {
139+
$iconSetAliases[$prefix] = $config['alias'];
140+
}
141+
if (isset($config['path'])) {
142+
$iconSetPaths[$prefix] = $config['path'];
143+
}
144+
}
145+
97146
$container->getDefinition('.ux_icons.local_svg_icon_registry')
98147
->setArguments([
99148
$mergedConfig['icon_dir'],
149+
$iconSetPaths,
100150
])
101151
;
102152

@@ -107,6 +157,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
107157
$container->getDefinition('.ux_icons.icon_renderer')
108158
->setArgument(1, $mergedConfig['default_icon_attributes'])
109159
->setArgument(2, $mergedConfig['aliases'])
160+
->setArgument(3, $iconSetAttributes)
110161
;
111162

112163
$container->getDefinition('.ux_icons.twig_icon_runtime')
@@ -117,8 +168,10 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
117168
$loader->load('iconify.php');
118169

119170
$container->getDefinition('.ux_icons.iconify')
120-
->setArgument(1, $mergedConfig['iconify']['endpoint'])
121-
;
171+
->setArgument(1, $mergedConfig['iconify']['endpoint']);
172+
173+
$container->getDefinition('.ux_icons.iconify_on_demand_registry')
174+
->setArgument(1, $iconSetAliases);
122175

123176
if (!$mergedConfig['iconify']['on_demand']) {
124177
$container->removeDefinition('.ux_icons.iconify_on_demand_registry');

src/Icons/src/IconRenderer.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@
1818
*/
1919
final class IconRenderer implements IconRendererInterface
2020
{
21+
/**
22+
* @param array<string, mixed> $defaultIconAttributes
23+
* @param array<string, string> $iconAliases
24+
* @param array<string, array<string, mixed>> $iconSetsAttributes
25+
*/
2126
public function __construct(
2227
private readonly IconRegistryInterface $registry,
2328
private readonly array $defaultIconAttributes = [],
24-
private readonly ?array $iconAliases = [],
29+
private readonly array $iconAliases = [],
30+
private readonly array $iconSetsAttributes = [],
2531
) {
2632
}
2733

@@ -36,11 +42,16 @@ public function __construct(
3642
*/
3743
public function renderIcon(string $name, array $attributes = []): string
3844
{
39-
$name = $this->iconAliases[$name] ?? $name;
45+
$iconName = $this->iconAliases[$name] ?? $name;
4046

41-
$icon = $this->registry->get($name)
42-
->withAttributes($this->defaultIconAttributes)
43-
->withAttributes($attributes);
47+
$icon = $this->registry->get($iconName);
48+
49+
if (0 < (int) $pos = strpos($name, ':')) {
50+
$setAttributes = $this->iconSetsAttributes[substr($name, 0, $pos)] ?? [];
51+
} elseif ($iconName !== $name && 0 < (int) $pos = strpos($iconName, ':')) {
52+
$setAttributes = $this->iconSetsAttributes[substr($iconName, 0, $pos)] ?? [];
53+
}
54+
$icon = $icon->withAttributes([...$this->defaultIconAttributes, ...($setAttributes ?? []), ...$attributes]);
4455

4556
foreach ($this->getPreRenderers() as $preRenderer) {
4657
$icon = $preRenderer($icon);

src/Icons/src/Registry/IconifyOnDemandRegistry.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@
2323
*/
2424
final class IconifyOnDemandRegistry implements IconRegistryInterface
2525
{
26-
public function __construct(private Iconify $iconify)
27-
{
26+
public function __construct(
27+
private Iconify $iconify,
28+
private ?array $prefixAliases = [],
29+
) {
2830
}
2931

3032
public function get(string $name): Icon
3133
{
3234
if (2 !== \count($parts = explode(':', $name))) {
3335
throw new IconNotFoundException(\sprintf('The icon name "%s" is not valid.', $name));
3436
}
37+
[$prefix, $icon] = $parts;
3538

36-
return $this->iconify->fetchIcon(...$parts);
39+
return $this->iconify->fetchIcon($this->prefixAliases[$prefix] ?? $prefix, $icon);
3740
}
3841
}

src/Icons/src/Registry/LocalSvgIconRegistry.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,38 @@
2323
*/
2424
final class LocalSvgIconRegistry implements IconRegistryInterface
2525
{
26-
public function __construct(private string $iconDir)
27-
{
26+
/**
27+
* @param array<string, string> $iconSetPaths
28+
*/
29+
public function __construct(
30+
private readonly string $iconDir,
31+
private readonly array $iconSetPaths = [],
32+
) {
2833
}
2934

3035
public function get(string $name): Icon
3136
{
32-
if (!file_exists($filename = \sprintf('%s/%s.svg', $this->iconDir, str_replace(':', '/', $name)))) {
33-
throw new IconNotFoundException(\sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
37+
if (str_contains($name, ':')) {
38+
[$prefix, $icon] = explode(':', $name, 2) + ['', ''];
39+
if ('' === $prefix || '' === $icon) {
40+
throw new IconNotFoundException(\sprintf('The icon name "%s" is not valid.', $name));
41+
}
42+
43+
if ($prefixPath = $this->iconSetPaths[$prefix] ?? null) {
44+
if (!file_exists($filename = $prefixPath.'/'.str_replace(':', '/', $icon).'.svg')) {
45+
throw new IconNotFoundException(\sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
46+
}
47+
48+
return Icon::fromFile($filename);
49+
}
50+
}
51+
52+
$filepath = str_replace(':', '/', $name).'.svg';
53+
if (file_exists($filename = $this->iconDir.'/'.$filepath)) {
54+
return Icon::fromFile($filename);
3455
}
3556

36-
return Icon::fromFile($filename);
57+
throw new IconNotFoundException(\sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
3758
}
3859

3960
public function has(string $name): bool

src/Icons/tests/Fixtures/TestKernel.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected function configureContainer(ContainerConfigurator $container): void
5050
]);
5151

5252
$container->extension('twig', [
53-
'default_path' => __DIR__ . '/templates',
53+
'default_path' => __DIR__.'/templates',
5454
]);
5555

5656
$container->extension('twig_component', [
@@ -60,6 +60,14 @@ protected function configureContainer(ContainerConfigurator $container): void
6060

6161
$container->extension('ux_icons', [
6262
'icon_dir' => '%kernel.project_dir%/tests/Fixtures/icons',
63+
'icon_sets' => [
64+
'fla' => [
65+
'path' => '%kernel.project_dir%/tests/Fixtures/images/flags',
66+
],
67+
'lu' => [
68+
'alias' => 'lucide',
69+
],
70+
],
6371
]);
6472

6573
$container->services()->set('logger', NullLogger::class);
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)