Skip to content

Commit 6f2e745

Browse files
committed
add IconifyOnDemandRegistry
1 parent db3ff6f commit 6f2e745

File tree

9 files changed

+104
-24
lines changed

9 files changed

+104
-24
lines changed

src/Icons/README.md

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,54 @@
66
composer require symfony/ux-icons
77
```
88

9-
## Add Icons
9+
## Icons?
1010

11-
No icons are provided by this package. Add your svg icons to the `assets/icons/` directory and commit them.
12-
The name of the file is used as the name of the icon (`name.svg` will be named `name`).
11+
No icons are provided by this package but there are several ways to include and render icons.
1312

14-
### Import Command
13+
### Local SVG Icons
1514

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.
15+
Add your svg icons to the `assets/icons/` directory and commit them.
16+
The name of the file is used as the _name_ of the icon (`name.svg` will be named `name`).
17+
If located in a subdirectory, the _name_ will be `sub-dir:name`.
18+
19+
### Iconify Icons
20+
21+
[Iconify Design](https://iconify.design/) is a huge searchable repository of icons from many different icon sets.
22+
This package provides a way to include any icon found on this site _on-demand_.
1923

2024
1. Visit [Iconify Design](https://icon-sets.iconify.design/) and search for an icon
2125
you'd like to use. Once you find one you like, visit the icon's profile page and use the widget
2226
to copy its name. For instance, https://icon-sets.iconify.design/flowbite/user-solid/ has the name
2327
`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:
28+
2. Just use this name in the [`ux_icon()`](#usage) function and the icon will be fetched (and cached)
29+
from the Iconify API.
30+
3. That's it!
31+
32+
> [!NOTE]
33+
> [Local SVG Icons](#local-svg-icons) of the same name will have precedence over _Iconify-on-demand_ icons.
34+
35+
#### Import Command
2636

27-
```bash
28-
bin/console ux:icons:import flowbite:user-solid # saved as `user-solid.svg` and name is `user-solid`
37+
You can import any icon from Iconify to your local directory using the `ux:icons:import` command:
2938

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-
```
39+
```bash
40+
bin/console ux:icons:import flowbite:user-solid # saved as `user-solid.svg` and name is `user-solid`
41+
42+
# adjust the local name
43+
bin/console ux:icons:import flowbite:user-solid@user # saved as `user.svg` and name is `user`
44+
45+
# import several at a time
46+
bin/console ux:icons:import flowbite:user-solid flowbite:home-solid
47+
```
3648

3749
## Usage
3850

3951
```twig
4052
{{ ux_icon('user-profile', {class: 'w-4 h-4'}) }} <!-- renders "user-profile.svg" -->
4153
4254
{{ ux_icon('sub-dir:user-profile', {class: 'w-4 h-4'}) }} <!-- renders "sub-dir/user-profile.svg" (sub-directory) -->
55+
56+
{{ ux_icon('flowbite:user-solid') }} <!-- renders "flowbite:user-solid" from Iconify -->
4357
```
4458

4559
### HTML Syntax
@@ -51,6 +65,8 @@ site.
5165
<twig:UX:Icon name="user-profile" class="w-4 h-4" /> <!-- renders "user-profile.svg" -->
5266

5367
<twig:UX:Icon name="sub-dir:user-profile" class="w-4 h-4" /> <!-- renders "sub-dir/user-profile.svg" (sub-directory) -->
68+
69+
<twig:UX:Icon name="flowbite:user-solid" /> <!-- renders "flowbite:user-solid" from Iconify -->
5470
```
5571

5672
## Caching

src/Icons/config/iconify.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313

1414
use Symfony\UX\Icons\Command\ImportIconCommand;
1515
use Symfony\UX\Icons\Iconify;
16+
use Symfony\UX\Icons\Registry\IconifyOnDemandRegistry;
1617

1718
return static function (ContainerConfigurator $container): void {
1819
$container->services()
20+
->set('.ux_icons.iconify_on_demand_registry', IconifyOnDemandRegistry::class)
21+
->args([
22+
service('.ux_icons.iconify'),
23+
])
24+
->tag('ux_icons.registry')
25+
1926
->set('.ux_icons.iconify', Iconify::class)
2027
->args([
2128
abstract_arg('endpoint'),

src/Icons/src/Iconify.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\UX\Icons;
1313

14+
use Symfony\Component\HttpClient\Exception\JsonException;
1415
use Symfony\Component\HttpClient\HttpClient;
1516
use Symfony\Component\HttpClient\ScopingHttpClient;
1617
use Symfony\Contracts\HttpClient\HttpClientInterface;
1718
use Symfony\UX\Icons\Exception\IconNotFoundException;
19+
use Symfony\UX\Icons\Svg\Icon;
1820

1921
/**
2022
* @author Kevin Bond <kevinbond@gmail.com>
@@ -33,15 +35,32 @@ public function __construct(
3335
throw new \LogicException('You must install "symfony/http-client" to use Iconify. Try running "composer require symfony/http-client".');
3436
}
3537

36-
$this->http = new ScopingHttpClient($http ?? HttpClient::create(), [
37-
'base_uri' => $endpoint,
38+
$this->http = ScopingHttpClient::forBaseUri($http ?? HttpClient::create(), $endpoint);
39+
}
40+
41+
public function fetchIcon(string $prefix, string $name): Icon
42+
{
43+
$response = $this->http->request('GET', sprintf('/%s.json?icons=%s', $prefix, $name));
44+
45+
try {
46+
$data = $response->toArray();
47+
} catch (JsonException) {
48+
throw new IconNotFoundException(sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
49+
}
50+
51+
if (!isset($data['icons'][$name]['body'])) {
52+
throw new IconNotFoundException(sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
53+
}
54+
55+
return new Icon($data['icons'][$name]['body'], [
56+
'viewBox' => sprintf('0 0 %s %s', $data['width'], $data['height']),
3857
]);
3958
}
4059

4160
public function fetchSvg(string $prefix, string $name): string
4261
{
4362
$content = $this->http
44-
->request('GET', sprintf('https://api.iconify.design/%s/%s.svg', $prefix, $name))
63+
->request('GET', sprintf('/%s/%s.svg', $prefix, $name))
4564
->getContent()
4665
;
4766

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Registry;
13+
14+
use Symfony\UX\Icons\Exception\IconNotFoundException;
15+
use Symfony\UX\Icons\Iconify;
16+
use Symfony\UX\Icons\IconRegistryInterface;
17+
use Symfony\UX\Icons\Svg\Icon;
18+
19+
/**
20+
* @author Kevin Bond <kevinbond@gmail.com>
21+
*
22+
* @internal
23+
*/
24+
final class IconifyOnDemandRegistry implements IconRegistryInterface
25+
{
26+
public function __construct(private Iconify $iconify)
27+
{
28+
}
29+
30+
public function get(string $name): Icon
31+
{
32+
if (2 !== \count($parts = explode(':', $name))) {
33+
throw new IconNotFoundException(sprintf('The icon name "%s" is not valid.', $name));
34+
}
35+
36+
return $this->iconify->fetchIcon(...$parts);
37+
}
38+
}

src/Icons/src/Registry/LocalSvgIconRegistry.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ public function __construct(private string $iconDir)
3030

3131
public function get(string $name): Icon
3232
{
33-
if (!Icon::isValidName($name)) {
34-
throw new IconNotFoundException(sprintf('The icon name "%s" is not valid.', $name));
35-
}
36-
3733
if (!file_exists($filename = sprintf('%s/%s.svg', $this->iconDir, str_replace(':', '/', $name)))) {
3834
throw new IconNotFoundException(sprintf('The icon "%s" (%s) does not exist.', $name, $filename));
3935
}

src/Icons/tests/Fixtures/templates/template1.html.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
<li id="fifth"><twig:UX:Icon name="user" class="h-8 w-8" /></li>
66
<li id="sixth"><twig:UX:Icon name="sub:check" /></li>
77
<li id="seventh"><twig:UX:Icon :name="'sub:'~'check'" /></li>
8+
<li id="eighth">{{ ux_icon('iconamoon:3d-duotone') }}</li>
89
</ul>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
{{ ux_icon('something:invalid') }}
22
{{ ux_icon('invalid') }}
3+
{{ ux_icon('iconamoon:invalid') }}

src/Icons/tests/Integration/Command/WarmCacheCommandTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function testCanWarmCache(): void
2828
->assertOutputContains('Warming the icon cache...')
2929
->assertOutputContains('Warmed icon user.')
3030
->assertOutputContains('Warmed icon sub:check.')
31+
->assertOutputContains('Warmed icon iconamoon:3d-duotone.')
3132
->assertOutputContains('Icon cache warmed.')
3233
;
3334
}

src/Icons/tests/Integration/RenderIconsInTwigTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function testRenderIcons(): void
3131
<li id="fifth"><svg viewBox="0 0 24 24" fill="currentColor" class="h-8 w-8"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"></path></svg></li>
3232
<li id="sixth"><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>
3333
<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>
34+
<li id="eighth"><svg viewBox="0 0 24 24" fill="currentColor"><g fill="none"><path fill="currentColor" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z" opacity=".16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v5l-4.33 2.5M12 12l4.33 2.5"/></g></svg></li>
3435
</ul>
3536
HTML,
3637
trim($output)

0 commit comments

Comments
 (0)