Skip to content

Commit 60fb1e4

Browse files
committed
Add Iconify chunk()
1 parent 6bab4bf commit 60fb1e4

File tree

2 files changed

+123
-5
lines changed

2 files changed

+123
-5
lines changed

src/Icons/src/Iconify.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,27 @@ final class Iconify
2727
{
2828
public const API_ENDPOINT = 'https://api.iconify.design';
2929

30+
// URL must be 500 chars max (iconify limit)
31+
// -39 chars: https://api.iconify.design/XXX.json?icons=
32+
// -safe margin
33+
private const int MAX_ICONS_QUERY_LENGTH = 400;
34+
3035
private HttpClientInterface $http;
3136
private \ArrayObject $sets;
37+
private int $maxIconsQueryLength;
3238

3339
public function __construct(
3440
private CacheInterface $cache,
3541
string $endpoint = self::API_ENDPOINT,
3642
?HttpClientInterface $http = null,
43+
?int $maxIconsQueryLength = null,
3744
) {
3845
if (!class_exists(HttpClient::class)) {
3946
throw new \LogicException('You must install "symfony/http-client" to use Iconify. Try running "composer require symfony/http-client".');
4047
}
4148

4249
$this->http = ScopingHttpClient::forBaseUri($http ?? HttpClient::create(), $endpoint);
50+
$this->maxIconsQueryLength = min(self::MAX_ICONS_QUERY_LENGTH, $maxIconsQueryLength ?? self::MAX_ICONS_QUERY_LENGTH);
4351
}
4452

4553
public function metadataFor(string $prefix): array
@@ -95,13 +103,10 @@ public function fetchIcons(string $prefix, array $names): array
95103
sort($names);
96104
$queryString = implode(',', $names);
97105
if (!preg_match('#^[a-z0-9-,]+$#', $queryString)) {
98-
throw new \InvalidArgumentException('Invalid icon names.');
106+
throw new \InvalidArgumentException('Invalid icon names.'.$queryString);
99107
}
100108

101-
// URL must be 500 chars max (iconify limit)
102-
// -39 chars: https://api.iconify.design/XXX.json?icons=
103-
// -safe margin
104-
if (450 < \strlen($prefix.$queryString)) {
109+
if (self::MAX_ICONS_QUERY_LENGTH < \strlen($prefix.$queryString)) {
105110
throw new \InvalidArgumentException('The query string is too long.');
106111
}
107112

@@ -155,6 +160,40 @@ public function searchIcons(string $prefix, string $query)
155160
return new \ArrayObject($response->toArray());
156161
}
157162

163+
/**
164+
* @return iterable<string[]>
165+
*/
166+
public function chunk(string $prefix, array $names): iterable
167+
{
168+
if (100 < ($prefixLength = strlen($prefix))) {
169+
throw new \InvalidArgumentException(sprintf('The icon prefix "%s" is too long.', $prefix));
170+
}
171+
172+
$maxLength = $this->maxIconsQueryLength - $prefixLength;
173+
174+
$curBatch = [];
175+
$curLength = 0;
176+
foreach ($names as $name) {
177+
if (100 < ($nameLength = strlen($name))) {
178+
throw new \InvalidArgumentException(sprintf('The icon name "%s" is too long.', $name));
179+
}
180+
if ($curLength && ($maxLength < ($curLength + $nameLength + 1))) {
181+
yield $curBatch;
182+
183+
$curBatch = [];
184+
$curLength = 0;
185+
}
186+
$curLength += $nameLength + 1;
187+
$curBatch[] = $name;
188+
}
189+
190+
if ($curLength) {
191+
yield $curBatch;
192+
}
193+
194+
yield from [];
195+
}
196+
158197
private function sets(): \ArrayObject
159198
{
160199
return $this->sets ??= $this->cache->get('ux-iconify-sets', function () {

src/Icons/tests/Unit/IconifyTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,85 @@ public function testGetMetadata(): void
238238
$this->assertSame('Font Awesome Solid', $metadata['name']);
239239
}
240240

241+
/**
242+
* @dataProvider provideChunkCases
243+
*/
244+
public function testChunk(int $maxQueryLength, string $prefix, array $names, array $chunks): void
245+
{
246+
$iconify = new Iconify(
247+
new NullAdapter(),
248+
'https://example.com',
249+
new MockHttpClient([]),
250+
$maxQueryLength,
251+
);
252+
253+
$this->assertSame($chunks, iterator_to_array($iconify->chunk($prefix, $names)));
254+
}
255+
256+
public static function provideChunkCases(): iterable
257+
{
258+
yield 'no icon should make no chunk' => [
259+
10,
260+
'ppppp',
261+
[],
262+
[],
263+
];
264+
265+
yield 'one icon should make one chunk' => [
266+
10,
267+
'ppppp',
268+
['aaaa1'],
269+
[['aaaa1']],
270+
];
271+
272+
yield 'two icons that should make two chunck' => [
273+
10,
274+
'ppppp',
275+
['aa1', 'aa2'],
276+
[['aa1'], ['aa2']],
277+
];
278+
279+
yield 'three icons that should make two chunck' => [
280+
15,
281+
'ppppp',
282+
['aaa1', 'aaa2', 'aaa3'],
283+
[['aaa1', 'aaa2'], ['aaa3']],
284+
];
285+
286+
yield 'four icons that should make two chunck' => [
287+
15,
288+
'ppppp',
289+
['aaaaaaaa1', 'a2', 'a3', 'a4'],
290+
[['aaaaaaaa1'], ['a2', 'a3', 'a4']],
291+
];
292+
}
293+
294+
public function testChunkThrowWithIconPrefixTooLong(): void
295+
{
296+
$iconify = new Iconify(new NullAdapter(), 'https://example.com', new MockHttpClient([]));
297+
298+
$prefix = str_pad('p', 101, 'p');
299+
$name = 'icon';
300+
301+
$this->expectExceptionMessage(sprintf('The icon prefix "%s" is too long.', $prefix));
302+
303+
// We need to iterate over the iterator to trigger the exception
304+
$result = iterator_to_array($iconify->chunk($prefix, [$name]));
305+
}
306+
307+
public function testChunkThrowWithIconNameTooLong(): void
308+
{
309+
$iconify = new Iconify(new NullAdapter(), 'https://example.com', new MockHttpClient([]));
310+
311+
$prefix = 'prefix';
312+
$name = str_pad('n', 101, 'n');
313+
314+
$this->expectExceptionMessage(sprintf('The icon name "%s" is too long.', $name));
315+
316+
// We need to iterate over the iterator to trigger the exception
317+
$result = iterator_to_array($iconify->chunk($prefix, [$name]));
318+
}
319+
241320
private function createHttpClient(mixed $data, int $code = 200): MockHttpClient
242321
{
243322
$mockResponse = new JsonMockResponse($data, ['http_code' => $code]);

0 commit comments

Comments
 (0)