Svg::configureOptions()   C
last analyzed

Complexity

Conditions 14
Paths 1

Size

Total Lines 103
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 74
CRAP Score 14

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 67
c 1
b 0
f 0
dl 0
loc 103
ccs 74
cts 74
cp 1
rs 5.7333
cc 14
nc 1
nop 1
crap 14

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
13
14
use enshrined\svgSanitize\Sanitizer;
15
use Ocubom\Twig\Extension\Svg\Exception\FileNotFoundException;
16
use Ocubom\Twig\Extension\Svg\Exception\ParseException;
17
use Ocubom\Twig\Extension\Svg\Processor\ApplyAttributesProcessor;
18
use Ocubom\Twig\Extension\Svg\Processor\ClassProcessor;
19
use Ocubom\Twig\Extension\Svg\Processor\CleanAttributesProcessor;
20
use Ocubom\Twig\Extension\Svg\Processor\PreserveAspectRatioProcessor;
21
use Ocubom\Twig\Extension\Svg\Processor\RemoveAttributesProcessor;
22
use Ocubom\Twig\Extension\Svg\Processor\TitleProcessor;
23
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
24
use Symfony\Component\OptionsResolver\Options;
25
use Symfony\Component\OptionsResolver\OptionsResolver;
26
27
use function BenTools\IterableFunctions\iterable_merge;
28
use function BenTools\IterableFunctions\iterable_to_array;
29
use function Ocubom\Twig\Extension\is_string;
30
31
class Svg implements SvgInterface
32
{
33
    use SvgTrait;
34
35
    private static array $processors = [];
36
37
    /**
38
     * @param Svg|\DOMNode|\SplFileInfo|\Stringable|string $data The SVG data
39
     */
40 14
    public function __construct($data, iterable $options = null)
41
    {
42
        switch (true) {
43 14
            case $data instanceof Svg : // "Copy" constructor
44
                $node = $this->constructFromString(DomUtil::toXml($data->svg)); // @codeCoverageIgnore
45
                break; // @codeCoverageIgnore
46
47 14
            case $data instanceof \DOMNode :
48 4
                $node = $this->constructFromString(DomUtil::toXml($data));
49 4
                break;
50
51 10
            case $data instanceof \SplFileInfo :
52 6
                $node = $this->constructFromFile($data);
53 5
                break;
54
55 5
            case is_string($data) :
56 5
                $node = $this->constructFromString($data);
57 5
                break;
58
59
            default:
60
                throw new ParseException(sprintf('Unable to create an SVG from "%s"', get_debug_type($data))); // @codeCoverageIgnore
61
        }
62
63
        // Merge options with SVG attributes
64 13
        $options = iterable_to_array(iterable_merge(
65 13
            DomUtil::getElementAttributes($node),
66 13
            $options ?? /* @scrutinizer ignore-type */ []
67 13
        ));
68
69
        // Define SVG attributes and resolve options + attributes
70 13
        $options = static::configureOptions()
71 13
            ->setDefined(array_keys($options)) // enables all optional options
72 13
            ->resolve($options);
73
74
        // Retrieve processors for this class with a naive cache
75 13
        $processors = self::$processors[static::class]
76 13
            ?? self::$processors[static::class] = call_user_func(function () {
77
                // Get processors array with default priority
78 3
                $processors = array_map(
79 3
                    function ($processor): array {
80 3
                        return is_callable($processor) ? [$processor, 0] : $processor;
81 3
                    },
82 3
                    static::getProcessors()
83 3
                );
84
85
                // Sort by priority
86 3
                usort($processors, function ($x, $y) {
87 3
                    return $x[1] <=> $y[1];
88 3
                });
89
90 3
                return array_column($processors, 0);
91 13
            });
92
93
        // Apply processors on a flesh clone in new DOM document
94 13
        $this->svg = array_reduce(
95 13
            $processors,
96 13
            function (\DOMElement $svg, callable $processor) use ($options): \DOMElement {
97 13
                return $processor($svg, $options);
98 13
            },
99 13
            DomUtil::cloneElement($node)
100 13
        );
101
    }
102
103 6
    protected function constructFromFile(\SplFileInfo $path): \DOMElement
104
    {
105 6
        $path = (string) $path;
106
107 6
        if (!is_file($path)) {
108
            throw new FileNotFoundException(sprintf('File "%s" does not exist.', $path)); // @codeCoverageIgnore
109
        }
110
111 6
        if (!is_readable($path)) {
112
            throw new FileNotFoundException(sprintf('File "%s" cannot be read.', $path)); // @codeCoverageIgnore
113
        }
114
115
        try {
116 6
            return $this->constructFromString(file_get_contents($path));
117 1
        } catch (ParseException $exc) {
118 1
            throw new ParseException(sprintf('File "%s" does not contain a valid SVG.', $path), 0, $exc);
119
        }
120
    }
121
122 14
    protected function constructFromString(string $contents): \DOMElement
123
    {
124
        // Sanitize contents (if enshrined\svgSanitize is installed)
125 14
        if (class_exists(Sanitizer::class)) {
126 14
            $contents = (new Sanitizer())->sanitize($contents) ?: $contents;
127
        }
128
129
        // Remove all namespaces
130 14
        $contents = preg_replace('@xmlns(:.*)?=(\"[^\"]*\"|\'[^\']*\')@Uis', '', $contents);
131 14
        if (empty($contents)) {
132 1
            throw new ParseException(sprintf('Invalid SVG string "%s".', func_get_arg(0)));
133
        }
134
135
        // Parse contents into DOM
136 13
        $doc = DomUtil::createDocument();
137 13
        if (false === $doc->loadXML($contents)) {
138
            throw new ParseException(sprintf('Unable to load SVG string "%s".', func_get_arg(0))); // @codeCoverageIgnore
139
        }
140
141
        // Get first svg item
142 13
        $node = $doc->getElementsByTagName('svg')->item(0);
143 13
        if ($node instanceof \DOMElement) {
144 13
            return $node; // Return first SVG element
145
        }
146
147
        throw new ParseException(sprintf('String "%s" does not contain any SVG.', func_get_arg(0))); // @codeCoverageIgnore
148
    }
149
150 3
    protected static function getProcessors(): array
151
    {
152 3
        return [
153
            // Apply options
154 3
            [new ApplyAttributesProcessor(), -1000],
155
156
            // Options will be ignored & removed
157
            // [new RemoveAttributesProcessor(), 1000],
158
159
            // Attributes with custom process
160 3
            new ClassProcessor(),
161 3
            new TitleProcessor(),
162 3
            new PreserveAspectRatioProcessor(),
163
164
            // Final touch
165 3
            [new CleanAttributesProcessor(), 10000],
166 3
        ];
167
    }
168
169
    /** @psalm-suppress MissingClosureParamType */
170 13
    protected static function configureOptions(OptionsResolver $resolver = null): OptionsResolver
171
    {
172 13
        $resolver = $resolver ?? new OptionsResolver();
173
174
        /** @psalm-suppress MissingClosureParamType */
175 13
        $normalizeClass = function (Options $options, $value): array {
176 13
            return array_filter(
177 13
                is_string($value) ? preg_split('@\s+@Uis', $value) : ($value ?? []),
178 13
                function (string $item): bool {
179 13
                    return !empty($item);
180 13
                }
181 13
            );
182 13
        };
183
184
        // Attributes
185
186 13
        $resolver->define('class')
187 13
            ->default('')
188 13
            ->allowedTypes('null', 'string', 'string[]')
189 13
            ->normalize($normalizeClass)
190 13
            ->info('Classes to apply');
191
192 13
        $resolver->define('class_block')
193 13
            ->default('')
194 13
            ->allowedTypes('null', 'string', 'string[]')
195 13
            ->normalize($normalizeClass)
196 13
            ->info('Classes to block');
197
198 13
        $resolver->define('width')
199 13
            ->default('1em')
200 13
            ->allowedTypes('string')
201 13
            ->info('Width of the element');
202
203 13
        $resolver->define('height')
204 13
            ->default('1em')
205 13
            ->allowedTypes('string')
206 13
            ->info('Height of the element');
207
208 13
        $resolver->define('title')
209 13
            ->default(null)
210 13
            ->allowedTypes('null', 'string')
211 13
            ->normalize(function (Options $options, ?string $value): ?string {
212 13
                return empty($value ?? '') ? null : $value;
213 13
            })
214 13
            ->info('Title of the icon. Used for semantic icons.');
215
216 13
        $resolver->define('focusable')
217 13
            ->default('false')
218 13
            ->allowedTypes('null', 'bool', 'string')
219 13
            ->normalize(function (Options $options, $value): ?string {
220 13
                if (is_bool($value)) {
221 1
                    return $value ? 'true' : 'false';
222
                }
223
224 13
                return empty($value) ? null : $value;
225 13
            })
226 13
            ->info('Indicates if the element can take the focus');
227
228 13
        $resolver->define('role')
229 13
            ->default('img')
230 13
            ->allowedTypes('null', 'string')
231 13
            ->info('Indicates the semantic meaning of the content');
232
233 13
        $resolver->define('aria-hidden')
234 13
            ->default(null)
235 13
            ->allowedTypes('null', 'bool', 'string')
236 13
            ->allowedValues(null, true, 'true', false, 'false')
237 13
            ->normalize(function (Options $options, $value): ?string {
238
                // Decorative icon: no title and not previously defined aria-hidden value
239
                if (
240 13
                    null === $value
241 13
                    && empty($options['aria-labelledby'])
242 13
                    && empty($options['title'])
243
                ) {
244 11
                    return 'true';
245
                }
246
247
                // Convert bool value into text
248 4
                if (is_bool($value)) {
249 1
                    return $value ? 'true' : 'false';
250
                }
251
252 4
                return empty($value)
253 4
                    ? null
254 4
                    : ('true' === $value ? 'true' : 'false');
255 13
            })
256 13
            ->info('Indicates whether the element is exposed to an accessibility API');
257
258 13
        $resolver->define('aria-labelledby')
259 13
            ->default(null)
260 13
            ->allowedTypes('null', 'string')
261 13
            ->normalize(function (Options $options, $value): ?string {
262
                // Decorative icon: no title and not previously defined aria-hidden value
263 13
                if (empty($options['title'])) {
264 12
                    return null;
265
                }
266
267
                // Generate an identifier based on title contents
268 4
                return $value ?? DomUtil::generateId(DomUtil::createElement('title', $options['title']));
269 13
            })
270 13
            ->info('Identifies the element that labels this element');
271
272 13
        return $resolver;
273
    }
274
}
275