Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Icons] improve DX when symfony/http-client is not installed #2678

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Icons/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.25.0

- Improve DX when `symfony/http-client` is not installed.

## 2.24.0

- Add `xmlns` attribute to icons downloaded with Iconify, to correctly render icons browser as an external file, in SVG editors, and in files explorers or text editors previews.
Expand Down
56 changes: 0 additions & 56 deletions src/Icons/config/iconify.php

This file was deleted.

39 changes: 39 additions & 0 deletions src/Icons/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\UX\Icons\Command\ImportIconCommand;
use Symfony\UX\Icons\Command\LockIconsCommand;
use Symfony\UX\Icons\Command\SearchIconCommand;
use Symfony\UX\Icons\Command\WarmCacheCommand;
use Symfony\UX\Icons\IconCacheWarmer;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Icons\IconRendererInterface;
use Symfony\UX\Icons\Registry\CacheIconRegistry;
use Symfony\UX\Icons\Registry\ChainIconRegistry;
use Symfony\UX\Icons\Registry\IconifyOnDemandRegistry;
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
use Symfony\UX\Icons\Twig\IconFinder;
use Symfony\UX\Icons\Twig\UXIconExtension;
Expand Down Expand Up @@ -86,5 +91,39 @@
service('.ux_icons.cache_warmer'),
])
->tag('console.command')

->set('.ux_icons.iconify', Iconify::class)
->args([
service('.ux_icons.cache'),
abstract_arg('endpoint'),
service('http_client')->nullOnInvalid(),
])

->set('.ux_icons.iconify_on_demand_registry', IconifyOnDemandRegistry::class)
->args([
service('.ux_icons.iconify'),
])
->tag('ux_icons.registry', ['priority' => -10])

->set('.ux_icons.command.import', ImportIconCommand::class)
->args([
service('.ux_icons.iconify'),
service('.ux_icons.local_svg_icon_registry'),
])
->tag('console.command')

->set('.ux_icons.command.lock', LockIconsCommand::class)
->args([
service('.ux_icons.iconify'),
service('.ux_icons.local_svg_icon_registry'),
service('.ux_icons.icon_finder'),
])
->tag('console.command')

->set('.ux_icons.command.search', SearchIconCommand::class)
->args([
service('.ux_icons.iconify'),
])
->tag('console.command')
;
};
24 changes: 24 additions & 0 deletions src/Icons/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,18 @@ the report to overwrite existing icons by using the ``--force`` option:

$ php bin/console ux:icons:lock --force

.. caution::

The process to find icons to lock in your Twig templates is imperfect. It
looks for any string that matches the pattern ``something:something`` so
it's probable there will be false positives. This command should not be used
to audit the icons in your templates in an automated way. Add ``-v`` see
*potential* invalid icons:

.. code-block:: terminal

$ php bin/console ux:icons:lock -v

Rendering Icons
---------------

Expand Down Expand Up @@ -472,6 +484,18 @@ In production, you can pre-warm the cache by running the following command:
This command looks in all your Twig templates for ``ux_icon()`` calls and
``<twig:ux:icon>`` tags and caches the icons it finds.

.. caution::

The process to find icons to cache in your Twig templates is imperfect. It
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe advise Lock command here? It shows details (-vv) on wether the IconSet does not exist (99% of the false positive extracted from templates) or if there is a real problem with an icon.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this caution admonition to the lock command section too.

looks for any string that matches the pattern ``something:something`` so
it's probable there will be false positives. This command should not be used
to audit the icons in your templates in an automated way. Add ``-v`` see
*potential* invalid icons:

.. code-block:: terminal

$ php bin/console ux:icons:warm-cache -v

.. caution::

Icons that have a name built dynamically will not be cached. It's advised to
Expand Down
5 changes: 5 additions & 0 deletions src/Icons/src/Command/WarmCacheCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->writeln(\sprintf(' Warmed icon <comment>%s</comment>.', $name));
}
},
onFailure: function (string $name, \Exception $e) use ($io) {
if ($io->isVerbose()) {
$io->writeln(\sprintf(' Failed to warm (potential) icon <error>%s</error>.', $name));
}
}
);

$io->success('Icon cache warmed.');
Expand Down
27 changes: 12 additions & 15 deletions src/Icons/src/DependencyInjection/UXIconsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\UX\Icons\Iconify;

/**
Expand Down Expand Up @@ -87,7 +86,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->arrayNode('iconify')
->info('Configuration for the remote icon service.')
->{interface_exists(HttpClientInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->canBeDisabled()
->children()
->booleanNode('on_demand')
->info('Whether to download icons "on demand".')
Expand Down Expand Up @@ -164,26 +163,24 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
->setArgument(1, $mergedConfig['ignore_not_found'])
;

if ($mergedConfig['iconify']['enabled']) {
$loader->load('iconify.php');
$container->getDefinition('.ux_icons.iconify')
->setArgument(1, $mergedConfig['iconify']['endpoint']);

$container->getDefinition('.ux_icons.iconify')
->setArgument(1, $mergedConfig['iconify']['endpoint']);
$container->getDefinition('.ux_icons.iconify_on_demand_registry')
->setArgument(1, $iconSetAliases);

$container->getDefinition('.ux_icons.iconify_on_demand_registry')
->setArgument(1, $iconSetAliases);
$container->getDefinition('.ux_icons.command.lock')
->setArgument(3, $mergedConfig['aliases'])
->setArgument(4, $iconSetAliases);

$container->getDefinition('.ux_icons.command.lock')
->setArgument(3, $mergedConfig['aliases'])
->setArgument(4, $iconSetAliases);

if (!$mergedConfig['iconify']['on_demand']) {
$container->removeDefinition('.ux_icons.iconify_on_demand_registry');
}
if (!$mergedConfig['iconify']['on_demand'] || !$mergedConfig['iconify']['enabled']) {
$container->removeDefinition('.ux_icons.iconify_on_demand_registry');
}

if (!$container->getParameter('kernel.debug')) {
$container->removeDefinition('.ux_icons.command.import');
$container->removeDefinition('.ux_icons.command.search');
$container->removeDefinition('.ux_icons.command.lock');
}
}
}
21 changes: 21 additions & 0 deletions src/Icons/src/Exception/HttpClientNotInstalledException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Icons\Exception;

/**
* @author Kevin Bond <[email protected]>
*
* @internal
*/
final class HttpClientNotInstalledException extends \LogicException
{
}
8 changes: 4 additions & 4 deletions src/Icons/src/IconCacheWarmer.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public function __construct(private CacheIconRegistry $registry, private IconFin
}

/**
* @param callable(string,Icon):void|null $onSuccess
* @param callable(string):void|null $onFailure
* @param callable(string,Icon):void|null $onSuccess
* @param callable(string,\Exception):void|null $onFailure
*/
public function warm(?callable $onSuccess = null, ?callable $onFailure = null): void
{
Expand All @@ -40,8 +40,8 @@ public function warm(?callable $onSuccess = null, ?callable $onFailure = null):
$icon = $this->registry->get($name, refresh: true);

$onSuccess($name, $icon);
} catch (IconNotFoundException) {
$onFailure($name);
} catch (IconNotFoundException $e) {
$onFailure($name, $e);
}
}
}
Expand Down
31 changes: 20 additions & 11 deletions src/Icons/src/Iconify.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\UX\Icons\Exception\HttpClientNotInstalledException;
use Symfony\UX\Icons\Exception\IconNotFoundException;

/**
Expand All @@ -39,15 +40,10 @@ final class Iconify

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

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

Expand All @@ -62,7 +58,7 @@ public function fetchIcon(string $prefix, string $name): Icon
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
}

$response = $this->http->request('GET', \sprintf('/%s.json?icons=%s', $prefix, $name));
$response = $this->http()->request('GET', \sprintf('/%s.json?icons=%s', $prefix, $name));

if (200 !== $response->getStatusCode()) {
throw new IconNotFoundException(\sprintf('The icon "%s:%s" does not exist on iconify.design.', $prefix, $name));
Expand Down Expand Up @@ -112,7 +108,7 @@ public function fetchIcons(string $prefix, array $names): array
throw new \InvalidArgumentException('The query string is too long.');
}

$response = $this->http->request('GET', \sprintf('/%s.json', $prefix), [
$response = $this->http()->request('GET', \sprintf('/%s.json', $prefix), [
'headers' => [
'Accept' => 'application/json',
],
Expand Down Expand Up @@ -158,7 +154,7 @@ public function getIconSets(): array

public function searchIcons(string $prefix, string $query)
{
$response = $this->http->request('GET', '/search', [
$response = $this->http()->request('GET', '/search', [
'query' => [
'query' => $query,
'prefix' => $prefix,
Expand Down Expand Up @@ -205,9 +201,22 @@ public function chunk(string $prefix, array $names): iterable
private function sets(): \ArrayObject
{
return $this->sets ??= $this->cache->get('iconify-sets', function () {
$response = $this->http->request('GET', '/collections');
$response = $this->http()->request('GET', '/collections');

return new \ArrayObject($response->toArray());
});
}

private function http(): HttpClientInterface
{
if (isset($this->http)) {
return $this->http;
}

if (!class_exists(HttpClient::class)) {
throw new HttpClientNotInstalledException('You must install "symfony/http-client" to use icons from ux.symfony.com/icons. Try running "composer require symfony/http-client".');
}

return $this->http = ScopingHttpClient::forBaseUri($this->httpClient ?? HttpClient::create(), $this->endpoint);
}
}
10 changes: 8 additions & 2 deletions src/Icons/src/Registry/ChainIconRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ public function get(string $name): Icon
foreach ($this->registries as $registry) {
try {
return $registry->get($name);
} catch (IconNotFoundException) {
} catch (IconNotFoundException $e) {
}
}

throw new IconNotFoundException(\sprintf('Icon "%s" not found.', $name));
$message = \sprintf('Icon "%s" not found.', $name);

if (isset($e)) {
$message .= " {$e->getMessage()}";
}

throw new IconNotFoundException($message, previous: $e ?? null);
}
}
7 changes: 6 additions & 1 deletion src/Icons/src/Registry/IconifyOnDemandRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\UX\Icons\Registry;

use Symfony\UX\Icons\Exception\HttpClientNotInstalledException;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\Iconify;
Expand All @@ -36,6 +37,10 @@ public function get(string $name): Icon
}
[$prefix, $icon] = $parts;

return $this->iconify->fetchIcon($this->prefixAliases[$prefix] ?? $prefix, $icon);
try {
return $this->iconify->fetchIcon($this->prefixAliases[$prefix] ?? $prefix, $icon);
} catch (HttpClientNotInstalledException $e) {
throw new IconNotFoundException($e->getMessage());
}
}
}
Loading
Loading