Skip to content

Commit f222b86

Browse files
committedMay 23, 2023
Improved error handling
Avoid aborting rendering when the SVG cannot be resolved and: * Uses interfaces in runtimes * Allow serialization of SVG files * Other small fixes.
1 parent 38d0565 commit f222b86

19 files changed

+229
-97
lines changed
 

‎.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ indent_size = 4
2626
insert_final_newline = true
2727
trim_trailing_whitespace = true
2828

29+
[*.test]
30+
trim_trailing_whitespace = false
31+
2932
[COMMIT_EDITMSG]
3033
max_line_length = 0

‎README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ The path is relative to the search path provided as the first argument when crea
144144
```php
145145
new \Ocubom\Twig\Extension\SvgRuntime(
146146
new \Ocubom\Twig\Extension\Svg\Provider\FileSystem\FileSystemLoader(
147-
'first/search/path',
148-
'second/search/path',
147+
new \Ocubom\Twig\Extension\Svg\Util\PathCollection(
148+
'first/search/path',
149+
'second/search/path',
150+
)
149151
)
150152
);
151153
```
@@ -159,7 +161,7 @@ The second argument can be used to add some attributes to the root element:
159161
}) }}
160162
```
161163

162-
#### `svg_symbols` filter
164+
#### `svg` (or `svg_symbols`) filter
163165

164166
This filter looks for embedded SVGs and converts each of them into a reference to a symbol.
165167

@@ -170,7 +172,7 @@ This filter looks for embedded SVGs and converts each of them into a reference t
170172
> If the filter is used in a fragment, an exception will be generated.
171173
172174
```twig
173-
{%- apply svg_symbols -%}
175+
{%- apply svg -%}
174176
<!DOCTYPE html>
175177
<html lang="en">
176178

‎src/Svg/Exception/Html5Exception.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Html5Exception extends RuntimeException
2323
public function __construct(string $message = null, int $code = 0, $previous = null)
2424
{
2525
parent::__construct(
26-
$this->formatMessage($message ?? 'Generated %d %s parsing HTML5', $previous),
26+
$this->formatMessage($message ?? 'Generated %d %s parsing HTML5', $previous, true),
2727
$code
2828
);
2929
}

‎src/Svg/Exception/JsonException.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of ocubom/twig-svg-extension
5+
*
6+
* © Oscar Cubo Medina <https://ocubom.github.io>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Ocubom\Twig\Extension\Svg\Exception;
13+
14+
class JsonException extends \JsonException implements Throwable
15+
{
16+
}

‎src/Svg/Provider/FontAwesome/FontAwesomeRuntime.php

+28-13
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,26 @@
1111

1212
namespace Ocubom\Twig\Extension\Svg\Provider\FontAwesome;
1313

14+
use Ocubom\Twig\Extension\Svg\Exception\LoaderException;
1415
use Ocubom\Twig\Extension\Svg\Loader\LoaderInterface;
1516
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
1617
use Ocubom\Twig\Extension\Svg\Util\Html5Util;
18+
use Psr\Log\LoggerInterface;
19+
use Psr\Log\NullLogger;
1720
use Twig\Environment;
1821
use Twig\Extension\RuntimeExtensionInterface;
1922

23+
use function BenTools\IterableFunctions\iterable_to_array;
24+
2025
class FontAwesomeRuntime implements RuntimeExtensionInterface
2126
{
2227
private LoaderInterface $loader;
28+
private LoggerInterface $logger;
2329

24-
public function __construct(FontAwesomeLoader $loader)
30+
public function __construct(LoaderInterface $loader, LoggerInterface $logger = null)
2531
{
2632
$this->loader = $loader;
33+
$this->logger = $logger ?? new NullLogger();
2734
}
2835

2936
public function replaceIcons(Environment $twig, string $html): string
@@ -44,21 +51,29 @@ function ($class) {
4451
/** @var \DOMNode $node */
4552
foreach (DomUtil::query($query, $doc) as $node) {
4653
if ($node instanceof \DOMElement) {
47-
if ($node->hasAttribute('data-fa-transform')) {
48-
continue; // Ignore icons with Power Transforms (use svg+js)
49-
}
54+
try {
55+
if ($node->hasAttribute('data-fa-transform')) {
56+
continue; // Ignore icons with Power Transforms (use svg+js)
57+
}
5058

51-
if ($twig->isDebug()) {
52-
DomUtil::createComment(DomUtil::toHtml($node), $node, true);
53-
}
59+
if ($twig->isDebug()) {
60+
DomUtil::createComment(DomUtil::toHtml($node), $node, true);
61+
}
5462

55-
$icon = $this->loader->resolve(
56-
$node->getAttribute('class'), // Resolve icon with class …
57-
DomUtil::getElementAttributes($node) // … and clone all its attributes as options
58-
);
63+
$icon = $this->loader->resolve(
64+
// Resolve icon with class …
65+
$node->getAttribute('class'),
66+
// … and clone all its attributes as options
67+
iterable_to_array(DomUtil::getElementAttributes($node))
68+
);
69+
70+
// Replace node
71+
DomUtil::replaceNode($node, $icon->getElement());
72+
} catch (LoaderException $err) {
73+
$this->logger->notice($err->getMessage(), ['exception' => $err]);
5974

60-
// Replace node
61-
DomUtil::replaceNode($node, $icon->getElement());
75+
DomUtil::removeNode($node);
76+
}
6277
}
6378
}
6479

‎src/Svg/Provider/FontAwesome/FontAwesomeSvg.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public function getHtmlTag(iterable $options = null): \DOMElement
123123
$this->getStyleClass(),
124124
'fa-'.$this->getName(),
125125
],
126-
'class_banned' => [
126+
'class_block' => [
127127
FontAwesome::INLINE_CLASS,
128128
],
129129
]);

‎src/Svg/Provider/Iconify/IconifyLoader.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Iconify\JSONTools\Collection;
1515
use Iconify\JSONTools\SVG as Icon;
16+
use Ocubom\Twig\Extension\Svg\Exception\JsonException;
1617
use Ocubom\Twig\Extension\Svg\Exception\LoaderException;
1718
use Ocubom\Twig\Extension\Svg\Exception\LogicException;
1819
use Ocubom\Twig\Extension\Svg\Loader\LoaderInterface;
@@ -84,7 +85,7 @@ private function loadIcon(string $prefix, string $name): ?Icon
8485

8586
$collection = new Collection();
8687
if (!$collection->loadFromFile($path, null, $this->getCacheFile($prefix))) {
87-
continue;
88+
throw new JsonException(sprintf('Unable to parse "%s" JSON file', $path));
8889
}
8990

9091
$data = $collection->getIconData($name);

‎src/Svg/Provider/Iconify/IconifyRuntime.php

+29-18
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
namespace Ocubom\Twig\Extension\Svg\Provider\Iconify;
1313

14+
use Ocubom\Twig\Extension\Svg\Exception\LoaderException;
1415
use Ocubom\Twig\Extension\Svg\Loader\LoaderInterface;
1516
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
1617
use Ocubom\Twig\Extension\Svg\Util\Html5Util;
18+
use Psr\Log\LoggerInterface;
19+
use Psr\Log\NullLogger;
1720
use Symfony\Component\OptionsResolver\Options;
1821
use Symfony\Component\OptionsResolver\OptionsResolver;
1922
use Twig\Environment;
@@ -26,12 +29,14 @@
2629
class IconifyRuntime implements RuntimeExtensionInterface
2730
{
2831
private LoaderInterface $loader;
29-
32+
private LoggerInterface $logger;
3033
private array $options;
3134

32-
public function __construct(IconifyLoader $loader, iterable $options = null)
35+
public function __construct(LoaderInterface $loader, iterable $options = null, LoggerInterface $logger = null)
3336
{
3437
$this->loader = $loader;
38+
$this->logger = $logger ?? new NullLogger();
39+
3540
$this->options = static::configureOptions()
3641
->resolve(iterable_to_array($options ?? /* @scrutinizer ignore-type */ []));
3742
}
@@ -47,24 +52,30 @@ public function replaceIcons(
4752
/** @var \DOMNode $node */
4853
foreach (iterable_to_array($this->queryIconify($doc, $options)) as $node) {
4954
if ($node instanceof \DOMElement) {
50-
if ($twig->isDebug()) {
51-
DomUtil::createComment(DomUtil::toHtml($node), $node, true);
52-
}
55+
try {
56+
$ident = $node->hasAttribute('data-icon')
57+
? $node->getAttribute('data-icon')
58+
: $node->getAttribute('icon');
5359

54-
$ident = $node->hasAttribute('data-icon')
55-
? $node->getAttribute('data-icon')
56-
: $node->getAttribute('icon');
60+
if ($twig->isDebug()) {
61+
DomUtil::createComment(DomUtil::toHtml($node), $node, true);
62+
}
5763

58-
$icon = $this->loader->resolve(
59-
$ident, // Resolve icon
60-
iterable_to_array(iterable_merge(
61-
DomUtil::getElementAttributes($node), // … and clone all its attributes as options
62-
$options
63-
))
64-
);
64+
$icon = $this->loader->resolve(
65+
$ident, // Resolve icon
66+
iterable_to_array(iterable_merge(
67+
DomUtil::getElementAttributes($node), // … and clone all its attributes as options
68+
$options
69+
))
70+
);
71+
72+
// Replace node
73+
DomUtil::replaceNode($node, $icon->getElement());
74+
} catch (LoaderException $err) {
75+
$this->logger->notice($err->getMessage(), ['exception' => $err]);
6576

66-
// Replace node
67-
DomUtil::replaceNode($node, $icon->getElement());
77+
DomUtil::removeNode($node);
78+
}
6879
}
6980
}
7081

@@ -109,7 +120,7 @@ function (string $class): string {
109120
if ($options['web_component']) {
110121
foreach ($options['web_component'] as $tag) {
111122
foreach ($doc->getElementsByTagName($tag) as $node) {
112-
if ($node instanceof \DOMElement && $node->hasAttribute('icon')) {
123+
if ($node->hasAttribute('icon')) {
113124
yield $node;
114125
}
115126
}

‎src/Svg/SvgTrait.php

+20
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Ocubom\Twig\Extension\Svg;
1313

14+
use Ocubom\Twig\Extension\Svg\Exception\ParseException;
1415
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
1516

1617
trait SvgTrait
@@ -26,4 +27,23 @@ public function __toString(): string
2627
{
2728
return DomUtil::toXml($this->svg);
2829
}
30+
31+
public function __serialize(): array
32+
{
33+
return ['svg' => (string) $this];
34+
}
35+
36+
public function __unserialize(array $data): void
37+
{
38+
$doc = DomUtil::createDocument();
39+
if (false === $doc->loadXML($data['svg'])) {
40+
throw new ParseException(sprintf('Unable to load SVG string "%s".', func_get_arg(0))); // @codeCoverageIgnore
41+
}
42+
43+
// Get first svg item
44+
$node = $doc->getElementsByTagName('svg')->item(0);
45+
assert($node instanceof \DOMElement);
46+
47+
$this->svg = $node;
48+
}
2949
}

‎src/Svg/Util/AggregatedExceptionTrait.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ trait AggregatedExceptionTrait
2121
/**
2222
* @param \Throwable|iterable<\Throwable> $previous
2323
*/
24-
private function formatMessage(string $message = null, $previous = null): string
24+
private function formatMessage(string $message = null, $previous = null, bool $previousMessages = false): string
2525
{
26-
$this->previous = $previous instanceof \Throwable
27-
? [$previous]
28-
: iterable_to_array($previous ?? /* @scrutinizer ignore-type */ [], false);
26+
$this->previous = $previous instanceof \Throwable ? [$previous] : iterable_to_array(
27+
$previous ?? /* @scrutinizer ignore-type */ [],
28+
false
29+
);
2930
$count = count($this->previous);
3031

3132
$message = sprintf(
@@ -34,7 +35,7 @@ private function formatMessage(string $message = null, $previous = null): string
3435
1 == $count ? 'exception' : 'exceptions'
3536
);
3637

37-
if ($count > 0) {
38+
if ($previousMessages && $count > 0) {
3839
$message .= ":\n\n";
3940
foreach ($this->previous as $idx => $err) {
4041
$message .= sprintf(

‎src/SvgExtension.php

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public function getFunctions(): array
7070
[SvgRuntime::class, 'embedSvg'],
7171
[
7272
'is_safe' => ['html'],
73+
'needs_environment' => true,
7374
]
7475
),
7576

‎src/SvgRuntime.php

+27-18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Ocubom\Twig\Extension;
1313

14+
use Ocubom\Twig\Extension\Svg\Exception\LoaderException;
1415
use Ocubom\Twig\Extension\Svg\Loader\LoaderInterface;
1516
use Ocubom\Twig\Extension\Svg\Symbol;
1617
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
@@ -37,6 +38,9 @@ public function convertToSymbols(Environment $twig, string $html): string
3738
// Load HTML
3839
$doc = Html5Util::loadHtml($html);
3940

41+
/** @var \DOMElement[] $elements */
42+
$elements = [];
43+
4044
/** @var \DOMElement[] $symbols */
4145
$symbols = [];
4246

@@ -46,11 +50,12 @@ public function convertToSymbols(Environment $twig, string $html): string
4650
'debug' => $twig->isDebug(),
4751
]);
4852

49-
// Replace all SVG with use
53+
// Replace the SVG with reference
5054
DomUtil::replaceNode($svg, $symbol->getReference());
5155

5256
// Index symbol by id
53-
$symbols[$symbol->getId()] = $symbol->getElement();
57+
$symbols[$symbol->getId()] = $symbols[$symbol->getId()] ?? $symbol->getElement();
58+
$elements[] = $svg;
5459
}
5560

5661
// Dump all symbols
@@ -62,7 +67,7 @@ public function convertToSymbols(Environment $twig, string $html): string
6267
);
6368
$node->setAttribute('style', 'display:none');
6469

65-
// Add format on debug mode
70+
// Add format (duplicate previous node space) on debug mode
6671
if ($twig->isDebug()) {
6772
assert($node->previousSibling instanceof \DOMNode);
6873
DomUtil::appendChildNode($node->previousSibling, $node);
@@ -85,32 +90,36 @@ public function convertToSymbols(Environment $twig, string $html): string
8590
$doc->createTextNode("\n"),
8691
$node->parentNode
8792
);
88-
}
8993

90-
$this->logger->info('Converted {count} SVG to symbols', [
91-
'count' => count($symbols),
92-
]);
94+
$this->logger->info('Converted {element_count} SVG elements into {symbol_count} SVG symbols', [
95+
'element_count' => count($elements),
96+
'element_items' => array_map([DomUtil::class, 'toXml'], $elements),
97+
'symbol_count' => count($symbols),
98+
'symbol_items' => array_map([DomUtil::class, 'toXml'], $symbols),
99+
]);
100+
}
93101
} elseif ($twig->isDebug()) {
94-
$this->logger->debug('No SVG to convert');
102+
$this->logger->debug('No SVG found');
95103
}
96104

97105
// Generate normalized HTML
98106
return Html5Util::toHtml($doc);
99107
}
100108

101-
public function embedSvg(string $ident, array $options = []): string
109+
public function embedSvg(Environment $twig, string $ident, array $options = []): string
102110
{
103-
$this->logger->debug('Resolving "{ident}"', [
104-
'ident' => $ident,
105-
'options' => $options,
106-
]);
111+
try {
112+
$svg = $this->loader->resolve($ident, $options);
107113

108-
$svg = $this->loader->resolve($ident, $options);
114+
return (string) $svg;
115+
} catch (LoaderException $err) {
116+
$this->logger->error($err->getMessage(), ['exception' => $err]);
109117

110-
$this->logger->debug('Render "{ident}" as inlined SVG', [
111-
'ident' => $ident,
112-
]);
118+
if ($twig->isDebug()) {
119+
return sprintf('<!--{{ svg("%s") }}-->', $ident);
120+
}
121+
}
113122

114-
return (string) $svg;
123+
return '';
115124
}
116125
}

‎tests/Fixtures/filter/fontawesome-noexists.test

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
fontawesome filter throws exception if no SVG is found
2+
fontawesome filter comment tags if no SVG is found
33
--TEMPLATE--
44
{%- apply fontawesome -%}
55
<!DOCTYPE html>
@@ -20,5 +20,17 @@ fontawesome filter throws exception if no SVG is found
2020
{%- endapply -%}
2121
--DATA--
2222
return []
23-
--EXCEPTION--
24-
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template (""FileSystemLoader" cannot load SVG for "solid/does-not-exist" on "tests/Fixtures/Resources" loading SVG for "fa-solid fa-does-not-exist" by "FontAwesomeLoader".") in "index.twig" at line 2.
23+
--CONFIG--
24+
return ['debug' => true]
25+
--EXPECT--
26+
<!DOCTYPE html>
27+
<html lang="en"><head>
28+
<meta charset="utf-8">
29+
</head>
30+
31+
<body>
32+
33+
<!--<span class="fa-solid fa-does-not-exist" fill="red" opacity=".8"></span>-->
34+
35+
</body>
36+
</html>

‎tests/Fixtures/filter/iconify-invalid.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ iconify filter throws exception if invalid json is found
1919
--DATA--
2020
return []
2121
--EXCEPTION--
22-
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template (""IconifyLoader" cannot load SVG for "invalid:json".") in "index.twig" at line 2.
22+
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Unable to parse "tests/Fixtures/Resources/json/invalid.json" JSON file") in "index.twig" at line 2.

‎tests/Fixtures/filter/iconify-noexists-collection.test

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
iconify filter throws exception if no SVG collection is found
2+
iconify filter comment tags if no SVG collection is found
33
--TEMPLATE--
44
{%- apply iconify -%}
55
<!DOCTYPE html>
@@ -18,5 +18,17 @@ iconify filter throws exception if no SVG collection is found
1818
{%- endapply -%}
1919
--DATA--
2020
return []
21-
--EXCEPTION--
22-
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template (""IconifyLoader" cannot load SVG for "collection-does-not-exists:home".") in "index.twig" at line 2.
21+
--CONFIG--
22+
return ['debug' => true]
23+
--EXPECT--
24+
<!DOCTYPE html>
25+
<html lang="en"><head>
26+
<meta charset="utf-8">
27+
</head>
28+
29+
<body>
30+
31+
<!--<span class="iconify" data-icon="collection-does-not-exists:home"></span>-->
32+
33+
</body>
34+
</html>

‎tests/Fixtures/filter/iconify-noexists-icon.test

-22
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
iconify filter comment tags if no SVG is found
3+
--TEMPLATE--
4+
{%- apply iconify -%}
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
8+
<head>
9+
<meta charset="utf-8">
10+
</head>
11+
12+
<body>
13+
14+
<span class="iconify" data-icon="mdi:icon-does-not-exist"></span>
15+
16+
</body>
17+
</html>
18+
{%- endapply -%}
19+
--DATA--
20+
return []
21+
--CONFIG--
22+
return ['debug' => true]
23+
--EXPECT--
24+
<!DOCTYPE html>
25+
<html lang="en"><head>
26+
<meta charset="utf-8">
27+
</head>
28+
29+
<body>
30+
31+
<!--<span class="iconify" data-icon="mdi:icon-does-not-exist"></span>-->
32+
33+
</body>
34+
</html>

‎tests/Fixtures/function/svg-noexists.test

+16-7
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ svg function throws exception if no SVG is found
1010

1111
<body>
1212

13-
{{ svg('no-exists.svg') }}
13+
{{ svg('no-exists.svg') }}
1414

1515
</body>
1616
</html>
1717
--DATA--
1818
return []
19-
--EXCEPTION--
20-
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template (""ChainLoader" cannot load SVG for "no-exists.svg":
19+
--CONFIG--
20+
return ['debug' => true]
21+
--EXPECT--
22+
<!DOCTYPE html>
23+
<html lang="en">
24+
25+
<head>
26+
<meta charset="utf-8">
27+
</head>
2128

22-
1. [Ocubom\Twig\Extension\Svg\Exception\LoaderException] "FileSystemLoader" cannot load SVG for "no-exists.svg" on "tests/Fixtures/Resources".
23-
2. [Ocubom\Twig\Extension\Svg\Exception\LoaderException] "FileSystemLoader" cannot load SVG for "solid/no-exists.svg" on "tests/Fixtures/Resources" loading SVG for "no-exists.svg" by "FontAwesomeLoader".
24-
3. [Ocubom\Twig\Extension\Svg\Exception\LoaderException] "IconifyLoader" cannot load SVG for "no-exists.svg".
25-
") in "index.twig" at line 11.
29+
<body>
30+
31+
<!--{{ svg("no-exists.svg") }}-->
32+
33+
</body>
34+
</html>

‎tests/SvgExtensionTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Ocubom\Twig\Extension\Svg\Provider\FontAwesome\FontAwesomeRuntime;
1919
use Ocubom\Twig\Extension\Svg\Provider\Iconify\IconifyLoader;
2020
use Ocubom\Twig\Extension\Svg\Provider\Iconify\IconifyRuntime;
21+
use Ocubom\Twig\Extension\Svg\Svg;
2122
use Ocubom\Twig\Extension\Svg\Util\PathCollection;
2223
use Ocubom\Twig\Extension\SvgExtension;
2324
use Ocubom\Twig\Extension\SvgRuntime;
@@ -27,6 +28,13 @@
2728

2829
class SvgExtensionTest extends IntegrationTestCase
2930
{
31+
public function testSvgSerialization()
32+
{
33+
$svg = new Svg(new \SplFileInfo($this->getFixturesDir().'Resources/test.svg'));
34+
35+
$this->assertEquals((string) $svg, (string) unserialize(serialize($svg)));
36+
}
37+
3038
public function getFixturesDir(): string
3139
{
3240
return __DIR__.'/Fixtures/';

0 commit comments

Comments
 (0)
Please sign in to comment.