Repository: symfony/ux
Pull request #2351 (2024-11-07 21:15:30)

[Icons] Prepare lock / warmup optimization
Method needed for both
* lock/warmup optimization
* sprite generation
* search performances


Patch:
diff --git a/src/Icons/src/Iconify.php b/src/Icons/src/Iconify.php
index 757dacb2..e29f2c69 100644
--- a/src/Icons/src/Iconify.php
+++ b/src/Icons/src/Iconify.php
@@ -85,6 +85,54 @@ final class Iconify
         ]);
     }
 
+    public function fetchIcons(string $prefix, array $names): array
+    {
+        if (!isset($this->sets()[$prefix])) {
+            throw new IconNotFoundException(\sprintf('The icon set "%s" does not exist on iconify.design.', $prefix));
+        }
+
+        // Sort to enforce cache hits
+        sort($names);
+        $queryString = implode(',', $names);
+        if (!preg_match('#^[a-z0-9-,]+$#', $queryString)) {
+            throw new \InvalidArgumentException('Invalid icon names.');
+        }
+
+        // URL must be 500 chars max (iconify limit)
+        // -39 chars: https://api.iconify.design/XXX.json?icons=
+        // -safe margin
+        if (450 < \strlen($prefix.$queryString)) {
+            throw new \InvalidArgumentException('The query string is too long.');
+        }
+
+        $response = $this->http->request('GET', \sprintf('/%s.json', $prefix), [
+            'headers' => [
+                'Accept' => 'application/json',
+            ],
+            'query' => [
+                'icons' => strtolower($queryString),
+            ],
+        ]);
+
+        if (200 !== $response->getStatusCode()) {
+            throw new IconNotFoundException(\sprintf('The icon set "%s" does not exist on iconify.design.', $prefix));
+        }
+
+        $data = $response->toArray();
+
+        $icons = [];
+        foreach ($data['icons'] as $iconName => $iconData) {
+            $height = $iconData['height'] ?? $data['height'] ??= $this->sets()[$prefix]['height'] ?? null;
+            $width = $iconData['width'] ?? $data['width'] ??= $this->sets()[$prefix]['width'] ?? null;
+
+            $icons[$iconName] = new Icon($iconData['body'], [
+                'viewBox' => \sprintf('0 0 %d %d', $width ?? $height, $height ?? $width),
+            ]);
+        }
+
+        return $icons;
+    }
+
     public function hasIconSet(string $prefix): bool
     {
         return isset($this->sets()[$prefix]);


Test patch:
diff --git a/src/Icons/tests/Unit/IconifyTest.php b/src/Icons/tests/Unit/IconifyTest.php
index b9406836..3fafce3a 100644
--- a/src/Icons/tests/Unit/IconifyTest.php
+++ b/src/Icons/tests/Unit/IconifyTest.php
@@ -16,6 +16,7 @@ use Symfony\Component\Cache\Adapter\NullAdapter;
 use Symfony\Component\HttpClient\MockHttpClient;
 use Symfony\Component\HttpClient\Response\JsonMockResponse;
 use Symfony\UX\Icons\Exception\IconNotFoundException;
+use Symfony\UX\Icons\Icon;
 use Symfony\UX\Icons\Iconify;
 
 /**
@@ -162,6 +163,71 @@ class IconifyTest extends TestCase
         $iconify->fetchIcon('bi', 'heart');
     }
 
+    public function testFetchIcons(): void
+    {
+        $iconify = new Iconify(
+            cache: new NullAdapter(),
+            endpoint: 'https://example.com',
+            http: new MockHttpClient([
+                new JsonMockResponse([
+                    'bi' => [],
+                ]),
+                new JsonMockResponse([
+                    'icons' => [
+                        'heart' => [
+                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
+                            'height' => 17,
+                        ],
+                        'bar' => [
+                            'body' => '<path d="M0 0h24v24H0z" fill="none"/>',
+                            'height' => 17,
+                        ],
+                    ],
+                ]),
+            ]),
+        );
+
+        $icons = $iconify->fetchIcons('bi', ['heart', 'bar']);
+
+        $this->assertCount(2, $icons);
+        $this->assertSame(['heart', 'bar'], array_keys($icons));
+        $this->assertContainsOnlyInstancesOf(Icon::class, $icons);
+    }
+
+    public function testFetchIconsThrowsWithInvalidIconNames(): void
+    {
+        $iconify = new Iconify(
+            cache: new NullAdapter(),
+            endpoint: 'https://example.com',
+            http: new MockHttpClient([
+                new JsonMockResponse([
+                    'bi' => [],
+                ]),
+            ]),
+        );
+
+        $this->expectException(\InvalidArgumentException::class);
+
+        $iconify->fetchIcons('bi', ['à', 'foo']);
+    }
+
+    public function testFetchIconsThrowsWithTooManyIcons(): void
+    {
+        $iconify = new Iconify(
+            cache: new NullAdapter(),
+            endpoint: 'https://example.com',
+            http: new MockHttpClient([
+                new JsonMockResponse([
+                    'bi' => [],
+                ]),
+            ]),
+        );
+
+        $this->expectException(\InvalidArgumentException::class);
+
+        $iconify->fetchIcons('bi', array_fill(0, 50, '1234567890'));
+    }
+
     public function testGetMetadata(): void
     {
         $responseFile = __DIR__.'/../Fixtures/Iconify/collections.json';