diff --git a/src/Icons/src/Iconify.php b/src/Icons/src/Iconify.php index 757dacb245c..e29f2c69f5c 100644 --- a/src/Icons/src/Iconify.php +++ b/src/Icons/src/Iconify.php @@ -85,6 +85,54 @@ public function fetchIcon(string $prefix, string $name): Icon ]); } + 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]); diff --git a/src/Icons/tests/Unit/IconifyTest.php b/src/Icons/tests/Unit/IconifyTest.php index b940683660e..3fafce3abcc 100644 --- a/src/Icons/tests/Unit/IconifyTest.php +++ b/src/Icons/tests/Unit/IconifyTest.php @@ -16,6 +16,7 @@ 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 @@ public function testFetchIconThrowsWhenStatusCodeNot200(): void $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' => '', + 'height' => 17, + ], + 'bar' => [ + 'body' => '', + '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';