Passed
Push — master ( e07c5e...321f4b )
by Mr.
01:43
created

SvgInline   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 93
c 7
b 0
f 0
dl 0
loc 282
rs 10
wmc 25

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A __call() 0 6 1
A bootstrap() 0 6 1
A setSvgAttributes() 0 9 3
A file() 0 7 1
A loadSvg() 0 10 2
A __toString() 0 3 1
A setFill() 0 3 1
A setSvgProperties() 0 13 2
A render() 0 12 1
A fai() 0 6 1
A getPixelValue() 0 11 2
A setSvgSize() 0 22 4
A setFallbackIcon() 0 3 1
A removeDomNodes() 0 6 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace YiiRocks\SvgInline;
6
7
use DOMDocument;
8
use DOMElement;
9
use DOMXPath;
10
use Psr\Container\ContainerInterface;
11
use YiiRocks\SvgInline\Bootstrap\SvgInlineBootstrapInterface;
12
use YiiRocks\SvgInline\FontAwesome\SvgInlineFontAwesomeInterface;
13
use Yiisoft\Aliases\Aliases;
14
use Yiisoft\Html\Html;
15
16
use function explode;
17
use function libxml_clear_errors;
18
use function libxml_use_internal_errors;
19
use function round;
20
use function ucfirst;
21
22
/**
23
 * SvgInline provides a quick and easy way to access icons.
24
 */
25
class SvgInline implements SvgInlineInterface
26
{
27
    /** @var array Values for converting various units to pixels */
28
    private const PIXEL_MAP = [
29
        'px' => 1,
30
        'em' => 16,
31
        'ex' => 16 / 2,
32
        'pt' => 16 / 12,
33
        'pc' => 16,
34
        'in' => 16 * 6,
35
        'cm' => 16 / (2.54 / 6),
36
        'mm' => 16 / (25.4 / 6),
37
    ];
38
39
    /** @var Aliases Object used to resolve aliases */
40
    protected Aliases $aliases;
41
42
    /** @var array Class property */
43
    protected array $class;
44
45
    /** @var string Backup icon in case requested icon cannot be found */
46
    protected string $fallbackIcon;
47
48
    /** @var string Color of the icon. Set to empty string to disable this attribute */
49
    protected string $fill;
50
51
    /** @var int height of the svg */
52
    protected int $svgHeight;
53
54
    /** @var array additional properties for the icon not set with Options */
55
    protected array $svgProperties;
56
57
    /** @var int width of the svg */
58
    protected int $svgWidth;
59
60
    /** $var ContainerInterface $container */
61
    private ContainerInterface $container;
62
63
    /** @var IconInterface icon properties */
64
    private IconInterface $icon;
65
66
    /** @var DOMDocument SVG file */
67
    private DOMDocument $svg;
68
69
    /** @var DOMElement SVG */
70
    private DOMElement $svgElement;
71
72
    /**
73
     * @param Aliases $aliases
74
     * @param ContainerInterface $container
75
     */
76
    public function __construct(Aliases $aliases, ContainerInterface $container)
77
    {
78
        $this->aliases = $aliases;
79
        $this->container = $container;
80
    }
81
82
    /**
83
     * Magic function, sets icon properties.
84
     *
85
     * @param string $name  property name
86
     * @param array  $value property value
87
     * @return self updated object
88
     */
89
    public function __call(string $name, $value): SvgInlineInterface
90
    {
91
        $new = clone $this;
92
        $function = 'set' . ucfirst($name);
93
        $new->icon->$function($value[0]);
94
        return $new;
95
    }
96
97
    /**
98
     * Magic function, call render to return the SVG string.
99
     *
100
     * @return string SVG data
101
     */
102
    public function __toString(): string
103
    {
104
        return $this->render();
105
    }
106
107
    /**
108
     * Sets the Bootstrap Icon
109
     *
110
     * @param string $name name of the icon
111
     * @return SvgInlineInterface component object
112
     */
113
    public function bootstrap(string $name): SvgInlineInterface
114
    {
115
        $bootstrap = $this->container->get(SvgInlineBootstrapInterface::class);
116
        $bootstrap->icon = $bootstrap->name($name);
117
118
        return $bootstrap;
119
    }
120
121
    /**
122
     * Sets the Font Awesome Icon
123
     *
124
     * @param string $name name of the icon
125
     * $param null|string $style style of the icon
126
     * @return SvgInlineInterface component object
127
     */
128
    public function fai(string $name, ?string $style = null): SvgInlineInterface
129
    {
130
        $fai = $this->container->get(SvgInlineFontAwesomeInterface::class);
131
        $fai->icon = $fai->name($name, $style);
132
133
        return $fai;
134
    }
135
136
    /**
137
     * Sets the filename
138
     *
139
     * @param string $file name of the icon, or filename
140
     * @return SvgInlineInterface component object
141
     */
142
    public function file(string $file): SvgInlineInterface
143
    {
144
        $this->icon = new Icon();
145
        $fileName = $this->aliases->get($file);
146
        $this->icon->setName($fileName);
0 ignored issues
show
Bug introduced by
The method setName() does not exist on YiiRocks\SvgInline\IconInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to YiiRocks\SvgInline\IconInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

146
        $this->icon->/** @scrutinizer ignore-call */ 
147
                     setName($fileName);
Loading history...
147
148
        return $this;
149
    }
150
151
    /**
152
     * Load Font Awesome SVG file. Falls back to default if not found.
153
     *
154
     * @see $fallbackIcon
155
     */
156
    public function loadSvg(): void
157
    {
158
        $iconFile = $this->icon->get('name');
159
        if (!$this->svg->load($iconFile, LIBXML_NOBLANKS)) {
160
            $this->svg->load($this->fallbackIcon, LIBXML_NOBLANKS);
161
        }
162
163
        $this->removeDomNodes($this->svg, '//comment()');
164
        $this->svgElement = $this->svg->getElementsByTagName('svg')->item(0);
165
        $this->class = ['class' => $this->icon->get('class')];
166
    }
167
168
    /**
169
     * Returns the SVG string.
170
     *
171
     * @return string SVG data
172
     */
173
    public function render(): string
174
    {
175
        libxml_clear_errors();
176
        libxml_use_internal_errors(true);
177
        $this->svg = new DOMDocument();
178
179
        $this->loadSvg();
180
        $this->setSvgSize();
181
        $this->setSvgProperties();
182
        $this->setSvgAttributes();
183
184
        return $this->svg->saveXML($this->svgElement);
185
    }
186
187
    /**
188
     * @see $fallbackIcon
189
     * @param string $value
190
     * @return void
191
     */
192
    public function setFallbackIcon(string $value): void
193
    {
194
        $this->fallbackIcon = $this->aliases->get($value);
195
    }
196
197
    /**
198
     * @see $fill
199
     * @param string $value
200
     * @return void
201
     */
202
    public function setFill(string $value): void
203
    {
204
        $this->fill = $value;
205
    }
206
207
    /**
208
     * Determines size of the SVG element.
209
     *
210
     * @return void
211
     */
212
    protected function setSvgSize(): void
213
    {
214
        $this->svgWidth = $this->getPixelValue($this->svgElement->getAttribute('width'));
215
        $this->svgHeight = $this->getPixelValue($this->svgElement->getAttribute('height'));
216
        $this->svgProperties['width'] = $this->svgWidth;
217
        $this->svgProperties['height'] = $this->svgHeight;
218
219
        if ($this->svgElement->hasAttribute('viewBox')) {
220
            [$xStart, $yStart, $xEnd, $yEnd] = explode(' ', $this->svgElement->getAttribute('viewBox'));
221
            $this->svgWidth = (int) $xEnd - (int) $xStart;
222
            $this->svgHeight = (int) $yEnd - (int) $yStart;
223
224
            $this->svgElement->removeAttribute('width');
225
            $this->svgElement->removeAttribute('height');
226
            unset($this->svgProperties['width'], $this->svgProperties['height']);
227
        }
228
229
        $width = $this->icon->get('width');
230
        $height = $this->icon->get('height');
231
        if ($width || $height) {
232
            $this->svgProperties['width'] = $width ?? round($height * $this->svgWidth / $this->svgHeight);
233
            $this->svgProperties['height'] = $height ?? round($width * $this->svgHeight / $this->svgWidth);
234
        }
235
    }
236
237
    /**
238
     * Converts various sizes to pixels.
239
     *
240
     * @param string $size
241
     * @return int
242
     */
243
    private function getPixelValue(string $size): int
244
    {
245
        $trimmedSize = trim($size);
246
        $value = (int) $trimmedSize;
247
        $unit = substr($trimmedSize, -2);
248
249
        if (isset(self::PIXEL_MAP[$unit])) {
250
            $trimmedSize = $value * self::PIXEL_MAP[$unit];
251
        }
252
253
        return (int) round((float) $trimmedSize);
254
    }
255
256
    /**
257
     * Removes nodes from a DOMDocument
258
     *
259
     * @return void
260
     */
261
    private function removeDomNodes(DOMDocument $dom, string $expression): void
262
    {
263
        $xpath = new DOMXPath($dom);
264
        while ($node = $xpath->query($expression)->item(0)) {
265
            if ($node->parentNode) {
266
                $node->parentNode->removeChild($node);
267
            }
268
        }
269
    }
270
271
    /**
272
     * Adds the properties to the SVG.
273
     *
274
     * @return void
275
     */
276
    private function setSvgAttributes(): void
277
    {
278
        $titleElement = $this->svg->createElement('title', $this->icon->getTitle());
279
        $this->svgElement->insertBefore($titleElement, $this->svgElement->firstChild);
280
281
        foreach ($this->svgProperties as $key => $value) {
282
            $this->svgElement->removeAttribute($key);
283
            if (!empty($value)) {
284
                $this->svgElement->setAttribute($key, (string) $value);
285
            }
286
        }
287
    }
288
289
    /**
290
     * Prepares the values to be set on the SVG.
291
     *
292
     * @return void
293
     */
294
    private function setSvgProperties(): void
295
    {
296
        $this->svgProperties['aria-hidden'] = 'true';
297
        $this->svgProperties['role'] = 'img';
298
        $this->svgProperties['id'] = $this->icon->get('id');
299
        $this->svgProperties['class'] = $this->class['class'];
300
301
        $css = $this->icon->get('css');
302
        if (is_array($css)) {
303
            $this->svgProperties['style'] = Html::cssStyleFromArray($css);
304
        }
305
306
        $this->svgProperties['fill'] = $this->icon->get('fill') ?? $this->fill;
307
    }
308
}
309