Repository: symfony/ux
Pull request #2467 (2024-12-25 23:50:29)

[Icons] Fix aliases support when fetching multiples icons with Iconify
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

Use case:
1. I need to download three icons
2. I go to https://ux.symfony.com/icons, and select "Material Design Icons"
3. I search for `capsule`, `sign` and `glasses`
4. https://ux.symfony.com/icons suggests me to run `symfony console ux:icon:import mdi:capsule`, `symfony console ux:icon:import mdi:sign` and `symfony console ux:icon:import mdi:glasses` (can be shortened to `symfony console ux:icon:import mdi:capsule mdi:sign mdi:glasses`)
5. But it fails, it tells me icons `mdi:capsule` and `mdi:sign` do not exist

In fact, they exist, but those icons are alias pointing to another icon, see https://api.iconify.design/mdi.json?icons=capsule%2Cglasses%2Csign: 
```json
{
  "prefix": "mdi",
  "lastModified": 1718880438,
  "aliases": {
    "capsule": {
      "parent": "pill"
    },
    "sign": {
      "parent": "draw"
    }
  },
  "width": 24,
  "height": 24,
  "icons": {
    "pill": {
      "body": "<path fill=\"currentColor\" d=\"m4.22 11.29l7.07-7.07a6.01 6.01 0 0 1 8.49 0c2.34 2.34 2.34 6.14 0 8.49l-7.07 7.07c-2.35 2.34-6.15 2.34-8.49 0a6.01 6.01 0 0 1 0-8.49m1.42 1.42a3.98 3.98 0 0 0-1.04 3.86l5.99-5.98l4.24 4.24l3.53-3.54a3.98 3.98 0 0 0 0-5.65a3.98 3.98 0 0 0-5.65 0z\"/>"
    },
    "glasses": {
      "body": "<path fill=\"currentColor\" d=\"M3 10c-.24 0-.45.09-.59.25c-.14.15-.2.37-.17.61l.5 2.99C2.82 14.5 3.4 15 4 15h3c.64 0 1.36-.56 1.5-1.18l1.06-3.19c.04-.13.01-.32-.06-.44c-.11-.12-.28-.19-.5-.19zm4 7H4C2.38 17 .96 15.74.76 14.14l-.5-2.99C.15 10.3.39 9.5.91 8.92S2.19 8 3 8h6c.83 0 1.58.35 2.06.96c.11.15.21.31.29.49c.43-.09.87-.09 1.29 0c.08-.18.18-.34.3-.49C13.41 8.35 14.16 8 15 8h6c.81 0 1.57.34 2.09.92c.51.58.75 1.38.65 2.19l-.51 3.07C23.04 15.74 21.61 17 20 17h-3c-1.56 0-3.08-1.19-3.46-2.7l-.9-2.71c-.38-.28-.91-.28-1.29 0l-.92 2.78C10.07 15.82 8.56 17 7 17m8-7c-.22 0-.39.07-.5.19c-.08.12-.1.31-.05.51l1.01 3.05c.18.69.9 1.25 1.54 1.25h3c.59 0 1.18-.5 1.25-1.11l.51-3.07c.03-.2-.03-.42-.17-.57A.77.77 0 0 0 21 10z\"/>"
    },
    "draw": {
      "body": "<path fill=\"currentColor\" d=\"M9.75 20.85c1.78-.7 1.39-2.63.49-3.85c-.89-1.25-2.12-2.11-3.36-2.94A9.8 9.8 0 0 1 4.54 12c-.28-.33-.85-.94-.27-1.06c.59-.12 1.61.46 2.13.68c.91.38 1.81.82 2.65 1.34l1.01-1.7C8.5 10.23 6.5 9.32 4.64 9.05c-1.06-.16-2.18.06-2.54 1.21c-.32.99.19 1.99.77 2.77c1.37 1.83 3.5 2.71 5.09 4.29c.34.33.75.72.95 1.18c.21.44.16.47-.31.47c-1.24 0-2.79-.97-3.8-1.61l-1.01 1.7c1.53.94 4.09 2.41 5.96 1.79m11.09-15.6c.22-.22.22-.58 0-.79l-1.3-1.3a.56.56 0 0 0-.78 0l-1.02 1.02l2.08 2.08M11 10.92V13h2.08l6.15-6.15l-2.08-2.08z\"/>"
    }
  }
}
```

But now, imagine I have ~120 icons to import but only ~80 of them have been imported... 😅 

This PR add support for using aliases when fetching **multiple** icons (it already exist, but when fetching **one** icon).

Kocal: No one was sleeping or what? 😆 

Patch:
diff --git a/src/Icons/src/Iconify.php b/src/Icons/src/Iconify.php
index 564b87f2..fcbebe81 100644
--- a/src/Icons/src/Iconify.php
+++ b/src/Icons/src/Iconify.php
@@ -126,7 +126,12 @@ final class Iconify
         $data = $response->toArray();
 
         $icons = [];
-        foreach ($data['icons'] as $iconName => $iconData) {
+        foreach ($names as $iconName) {
+            $iconData = $data['icons'][$data['aliases'][$iconName]['parent'] ?? $iconName] ?? null;
+            if (!$iconData) {
+                continue;
+            }
+
             $height = $iconData['height'] ?? $data['height'] ??= $this->sets()[$prefix]['height'] ?? null;
             $width = $iconData['width'] ?? $data['width'] ??= $this->sets()[$prefix]['width'] ?? null;
 


Test patch:
diff --git a/src/Icons/tests/Unit/IconifyTest.php b/src/Icons/tests/Unit/IconifyTest.php
index c6edef0c..c57e37cc 100644
--- a/src/Icons/tests/Unit/IconifyTest.php
+++ b/src/Icons/tests/Unit/IconifyTest.php
@@ -190,7 +190,47 @@ class IconifyTest extends TestCase
         $icons = $iconify->fetchIcons('bi', ['heart', 'bar']);
 
         $this->assertCount(2, $icons);
-        $this->assertSame(['heart', 'bar'], array_keys($icons));
+        $this->assertSame(['bar', 'heart'], array_keys($icons));
+        $this->assertContainsOnlyInstancesOf(Icon::class, $icons);
+    }
+
+    public function testFetchIconsByAliases(): void
+    {
+        $iconify = new Iconify(
+            cache: new NullAdapter(),
+            endpoint: 'https://example.com',
+            http: new MockHttpClient([
+                new JsonMockResponse([
+                    'mdi' => [],
+                ]),
+                new JsonMockResponse([
+                    'aliases' => [
+                        'capsule' => [
+                            'parent' => 'pill',
+                        ],
+                        'sign' => [
+                            'parent' => 'draw',
+                        ],
+                    ],
+                    'icons' => [
+                        'pill' => [
+                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
+                        ],
+                        'glasses' => [
+                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
+                        ],
+                        'draw' => [
+                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
+                        ],
+                    ],
+                ]),
+            ]),
+        );
+
+        $icons = $iconify->fetchIcons('mdi', ['capsule', 'sign', 'glasses']);
+
+        $this->assertCount(3, $icons);
+        $this->assertSame(['capsule', 'glasses', 'sign'], array_keys($icons));
         $this->assertContainsOnlyInstancesOf(Icon::class, $icons);
     }