Passed
Push — main ( 7d39cf...4a07e7 )
by Oscar
03:49
created

Svg::__construct()   B

Complexity

Conditions 11
Paths 33

Size

Total Lines 60
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 11.0761

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 37
c 1
b 0
f 0
dl 0
loc 60
ccs 32
cts 35
cp 0.9143
rs 7.3166
cc 11
nc 33
nop 2
crap 11.0761

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 function BenTools\IterableFunctions\iterable_merge;
15
use function BenTools\IterableFunctions\iterable_to_array;
16
17
use enshrined\svgSanitize\Sanitizer;
18
19
use function Ocubom\Twig\Extension\is_string;
20
21
use Ocubom\Twig\Extension\Svg\Exception\FileNotFoundException;
22
use Ocubom\Twig\Extension\Svg\Exception\ParseException;
23
use Ocubom\Twig\Extension\Svg\Processor\ClassProcessor;
24
use Ocubom\Twig\Extension\Svg\Processor\RemoveAttributeProcessor;
25
use Ocubom\Twig\Extension\Svg\Processor\TitleProcessor;
26
use Ocubom\Twig\Extension\Svg\Util\DomHelper;
27
use Symfony\Component\OptionsResolver\Options;
28
use Symfony\Component\OptionsResolver\OptionsResolver;
29
30
class Svg implements SvgInterface
31
{
32
    use SvgTrait;
33
34
    /** @var array<string, array<string, array<int, callable>|callable>> */
35
    private static array $processors = [];
36
37
    /**
38
     * @param mixed $data The SVG data
39
     */
40 8
    public function __construct($data, iterable $options = null)
41
    {
42
        switch (true) {
43 8
            case $data instanceof Svg : // "Copy" constructor
44
                $node = $this->constructFromString(DomHelper::toXml($data->svg)); // @codeCoverageIgnore
45
                break; // @codeCoverageIgnore
46
47 8
            case $data instanceof \DOMNode :
48 3
                $node = $this->constructFromString(DomHelper::toXml($data));
49 3
                break;
50
51 5
            case $data instanceof \SplFileInfo :
52 5
                $node = $this->constructFromFile($data);
53 4
                break;
54
55
            case is_string($data) :
56
                $node = $this->constructFromString($data);
57
                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 7
        $options = iterable_to_array(iterable_merge(
65 7
            DomHelper::getElementAttributes($node),
66 7
            $options ?? /* @scrutinizer ignore-type */ []
67 7
        ));
68
69
        // Define SVG attributes and resolve options + attributes
70 7
        $options = static::configureOptions()
71 7
            ->setDefined(array_keys($options)) // enables all optional options
72 7
            ->resolve($options);
73
74
        // Retrieve processors for this class with a naive cache
75 7
        $processors = self::$processors[static::class]
76 7
            ?? self::$processors[static::class] = array_map(
77 7
                function ($processors): array {
78 2
                    return is_iterable($processors) ? $processors : [$processors];
79 7
                },
80 7
                static::getProcessors()
81 7
            );
82 7
        if (isset($processors[''])) {
83 2
            $options[''] = null; // Force execution of global processors.
84
        }
85
86
        // Apply processors on a flesh clone in new DOM document
87 7
        $this->svg = DomHelper::cloneElement($node);
88 7
        foreach ($options as $key => $val) {
89 7
            if (isset($processors[$key])) {
90
                // Apply processors
91 7
                foreach ($processors[$key] as $processor) {
92 7
                    $this->svg = $processor($this->svg, $options);
93
                }
94 7
            } elseif (empty($val)) {
95
                // Remove empty attributes
96 4
                $this->svg->removeAttribute($key);
97
            } else {
98
                // Set attribute value
99 7
                $this->svg->setAttribute($key, $val);
100
            }
101
        }
102
    }
103
104 5
    protected function constructFromFile(\SplFileInfo $path): \DOMElement
105
    {
106 5
        $path = (string) $path;
107
108 5
        if (!is_file($path)) {
109
            throw new FileNotFoundException(sprintf('File "%s" does not exist.', $path)); // @codeCoverageIgnore
110
        }
111
112 5
        if (!is_readable($path)) {
113
            throw new FileNotFoundException(sprintf('File "%s" cannot be read.', $path)); // @codeCoverageIgnore
114
        }
115
116
        try {
117 5
            return $this->constructFromString(file_get_contents($path));
118 1
        } catch (ParseException $exc) {
119 1
            throw new ParseException(sprintf('File "%s" does not contain a valid SVG.', $path), 0, $exc);
120
        }
121
    }
122
123 8
    protected function constructFromString(string $contents): \DOMElement
124
    {
125
        // Sanitize contents (if enshrined\svgSanitize is installed)
126 8
        if (class_exists(Sanitizer::class)) {
127 8
            $contents = (new Sanitizer())->sanitize($contents);
128
        }
129
130
        // Remove all namespaces
131 8
        $contents = preg_replace('@xmlns(:.*)?=(\"[^\"]*\"|\'[^\']*\')@Uis', '', $contents);
132 8
        if (empty($contents)) {
133 1
            throw new ParseException(sprintf('Invalid SVG string "%s".', func_get_arg(0)));
134
        }
135
136
        // Parse contents into DOM
137 7
        $doc = DomHelper::createDocument();
138 7
        if (false === $doc->loadXML($contents)) {
139
            throw new ParseException(sprintf('Unable to load SVG string "%s".', func_get_arg(0))); // @codeCoverageIgnore
140
        }
141
142
        // Get first svg item
143 7
        $node = $doc->getElementsByTagName('svg')->item(0);
144 7
        if ($node instanceof \DOMElement) {
145 7
            return $node; // Return first SVG element
146
        }
147
148
        throw new ParseException(sprintf('String "%s" does not contain any SVG.', func_get_arg(0))); // @codeCoverageIgnore
149
    }
150
151
    /**
152
     * @param mixed $path The path to the SVG file
153
     *
154
     * @return static
155
     *
156
     * @psalm-suppress UnsafeInstantiation
157
     *
158
     * @deprecated since ocubom/twig-svg-extension 1.1, use Svg constructor with SplInfo argument instead
159
     *
160
     * @codeCoverageIgnore
161
     */
162
    public static function createFromFile($path, iterable $options = null): self
163
    {
164
        trigger_deprecation('ocubom/twig-svg-extension', '1.1', 'Using "%s" is deprecated, use "%s" constructor with SplInfo argument instead.', __METHOD__, Svg::class);
165
166
        return new static($path instanceof \SplFileInfo ? $path : new \SplFileInfo((string) $path), $options);
167
    }
168
169
    /**
170
     * @param mixed $contents The SVG contents as an "stringable"
171
     *
172
     * @return static
173
     *
174
     * @psalm-suppress UnsafeInstantiation
175
     *
176
     * @deprecated since ocubom/twig-svg-extension 1.1, use Svg constructor instead
177
     *
178
     * @codeCoverageIgnore
179
     */
180
    public static function createFromString($contents, iterable $options = null): self
181
    {
182
        trigger_deprecation('ocubom/twig-svg-extension', '1.1', 'Using "%s" is deprecated, use "%s" constructor instead.', __METHOD__, Svg::class);
183
184
        return new static((string) $contents, $options);
185
    }
186
187
    /**
188
     * @return array<string, array<int, callable>|callable>
189
     */
190 2
    protected static function getProcessors(): array
191
    {
192 2
        return [
193
            // Options will be ignored & removed
194 2
            'debug' => new RemoveAttributeProcessor('debug'),
195 2
            'minimize' => new RemoveAttributeProcessor('minimize'),
196 2
            'class_block' => new RemoveAttributeProcessor('class_block'),
197
198
            // Custom process
199 2
            'class' => new ClassProcessor(),
200 2
            'title' => new TitleProcessor(),
201
202
            // Ignore attributes that depends on other options
203 2
            'aria-labelledby' => [], // generated on TitleProcessor
204 2
        ];
205
    }
206
207
    /** @psalm-suppress MissingClosureParamType */
208 7
    protected static function configureOptions(OptionsResolver $resolver = null): OptionsResolver
209
    {
210 7
        $resolver = $resolver ?? new OptionsResolver();
211
212
        // Options
213
214 7
        $resolver->define('debug')
215 7
            ->default(false)
216 7
            ->allowedTypes('bool')
217 7
            ->info('Enable debug output');
218
219 7
        $resolver->define('minimize')
220 7
            ->default(true)
221 7
            ->allowedTypes('bool')
222 7
            ->info('Minimize output');
223
224
        // Attributes
225
226
        /** @psalm-suppress MissingClosureParamType */
227 7
        $normalizeClass = function (Options $options, $value): array {
228 7
            return array_filter(
229 7
                is_string($value) ? preg_split('@\s+@Uis', $value) : ($value ?? []),
230 7
                function (string $item): bool {
231 7
                    return !empty($item);
232 7
                }
233 7
            );
234 7
        };
235
236 7
        $resolver->define('class')
237 7
            ->default('')
238 7
            ->allowedTypes('null', 'string', 'string[]')
239 7
            ->normalize($normalizeClass)
240 7
            ->info('Classes to apply');
241
242 7
        $resolver->define('class_block')
243 7
            ->default('')
244 7
            ->allowedTypes('null', 'string', 'string[]')
245 7
            ->normalize($normalizeClass)
246 7
            ->info('Classes to block');
247
248 7
        $resolver->define('title')
249 7
            ->default(null)
250 7
            ->allowedTypes('null', 'string')
251 7
            ->normalize(function (Options $options, ?string $value): ?string {
252 7
                return empty($value ?? '') ? null : $value;
253 7
            })
254 7
            ->info('Title of the icon. Used for semantic icons.');
255
256 7
        $resolver->define('focusable')
257 7
            ->default('false')
258 7
            ->allowedTypes('null', 'bool', 'string')
259 7
            ->normalize(function (Options $options, $value): ?string {
260 7
                if (is_bool($value)) {
261 1
                    return $value ? 'true' : 'false';
262
                }
263
264 7
                return empty($value) ? null : $value;
265 7
            })
266 7
            ->info('Indicates if the element can take the focus');
267
268 7
        $resolver->define('role')
269 7
            ->default('img')
270 7
            ->allowedTypes('null', 'string')
271 7
            ->info('Indicates the semantic meaning of the content');
272
273 7
        $resolver->define('aria-hidden')
274 7
            ->default(null)
275 7
            ->allowedTypes('null', 'bool', 'string')
276 7
            ->allowedValues(null, true, 'true', false, 'false')
277 7
            ->normalize(function (Options $options, $value): ?string {
278
                // Decorative icon: no title and not previously defined aria-hidden value
279
                if (
280 7
                    null === $value
281 7
                    && empty($options['aria-labelledby'])
282 7
                    && empty($options['title'])
283
                ) {
284 5
                    return 'true';
285
                }
286
287
                // Convert bool value into text
288 4
                if (is_bool($value)) {
289 1
                    return $value ? 'true' : 'false';
290
                }
291
292 4
                return empty($value)
293 4
                    ? null
294 4
                    : ('true' === $value ? 'true' : 'false');
295 7
            })
296 7
            ->info('Indicates whether the element is exposed to an accessibility API');
297
298 7
        $resolver->define('aria-labelledby')
299 7
            ->default(null)
300 7
            ->allowedTypes('null', 'string')
301 7
            ->normalize(function (Options $options, $value): ?string {
302
                // Decorative icon: no title and not previously defined aria-hidden value
303 7
                if (empty($options['title'])) {
304 6
                    return null;
305
                }
306
307
                // Generate an identifier based on title contents
308 4
                return $value ?? Ident::generate(DomHelper::createElement('title', $options['title']));
309 7
            })
310 7
            ->info('Identifies the element that labels this element');
311
312 7
        return $resolver;
313
    }
314
}
315