Passed
Push — master ( ca3e0c...20be78 )
by Mr.
01:40
created

src/SvgInline.php (2 issues)

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 Object $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
        $function = 'set' . ucfirst($name);
92
        $this->icon->$function($value[0]);
93
        return $this;
94
    }
95
96
    /**
97
     * Magic function, returns the SVG string.
98
     *
99
     * @return string SVG data
100
     */
101
    public function __toString(): string
102
    {
103
        libxml_clear_errors();
104
        libxml_use_internal_errors(true);
105
        $this->svg = new DOMDocument();
106
107
        $this->loadSvg();
108
        $this->setSvgSize();
109
        $this->setSvgProperties();
110
        $this->setSvgAttributes();
111
112
        return $this->svg->saveXML($this->svgElement);
113
    }
114
115
    /**
116
     * Sets the Bootstrap Icon
117
     *
118
     * @param string $name name of the icon
119
     * @return SvgInlineInterface component object
120
     */
121
    public function bootstrap(string $name): SvgInlineInterface
122
    {
123
        $bootstrap = new Icon();
0 ignored issues
show
The assignment to $bootstrap is dead and can be removed.
Loading history...
124
        $bootstrap = $this->container->get(SvgInlineBootstrapInterface::class);
125
        $bootstrap->icon = $bootstrap->name($name);
126
127
        return $bootstrap;
128
    }
129
130
    /**
131
     * Sets the Font Awesome Icon
132
     *
133
     * @param string $name name of the icon
134
     * $param null|string $style style of the icon
135
     * @return SvgInlineInterface component object
136
     */
137
    public function fai(string $name, ?string $style = null): SvgInlineInterface
138
    {
139
        $bootstrap = new Icon();
0 ignored issues
show
The assignment to $bootstrap is dead and can be removed.
Loading history...
140
        $fai = $this->container->get(SvgInlineFontAwesomeInterface::class);
141
        $fai->icon = $fai->name($name, $style);
142
143
        return $fai;
144
    }
145
146
    /**
147
     * Sets the filename
148
     *
149
     * @param string $file name of the icon, or filename
150
     * @return SvgInlineInterface component object
151
     */
152
    public function file(string $file): SvgInlineInterface
153
    {
154
        $this->icon = new Icon();
155
        $fileName = $this->aliases->get($file);
156
        $this->icon->setName($fileName);
157
158
        return $this;
159
    }
160
161
    /**
162
     * Load Font Awesome SVG file. Falls back to default if not found.
163
     *
164
     * @see $fallbackIcon
165
     */
166
    public function loadSvg(): void
167
    {
168
        $iconFile = $this->icon->get('name');
169
        if (!$this->svg->load($iconFile, LIBXML_NOBLANKS)) {
170
            $this->svg->load($this->fallbackIcon, LIBXML_NOBLANKS);
171
        }
172
173
        $this->removeDomNodes($this->svg, '//comment()');
174
        $this->svgElement = $this->svg->getElementsByTagName('svg')->item(0);
175
        $this->class = ['class' => $this->icon->get('class')];
176
    }
177
178
    /**
179
     * @see $fallbackIcon
180
     * @param string $value
181
     * @return void
182
     */
183
    public function setFallbackIcon(string $value): void
184
    {
185
        $this->fallbackIcon = $this->aliases->get($value);
186
    }
187
188
    /**
189
     * @see $fill
190
     * @param string $value
191
     * @return void
192
     */
193
    public function setFill(string $value): void
194
    {
195
        $this->fill = $value;
196
    }
197
198
    /**
199
     * Determines size of the SVG element.
200
     *
201
     * @return void
202
     */
203
    protected function setSvgSize(): void
204
    {
205
        $this->svgWidth = $this->getPixelValue($this->svgElement->getAttribute('width'));
206
        $this->svgHeight = $this->getPixelValue($this->svgElement->getAttribute('height'));
207
        $this->svgProperties['width'] = $this->svgWidth;
208
        $this->svgProperties['height'] = $this->svgHeight;
209
210
        if ($this->svgElement->hasAttribute('viewBox')) {
211
            [$xStart, $yStart, $xEnd, $yEnd] = explode(' ', $this->svgElement->getAttribute('viewBox'));
212
            $this->svgWidth = (int) $xEnd - (int) $xStart;
213
            $this->svgHeight = (int) $yEnd - (int) $yStart;
214
215
            $this->svgElement->removeAttribute('width');
216
            $this->svgElement->removeAttribute('height');
217
            unset($this->svgProperties['width'], $this->svgProperties['height']);
218
        }
219
220
        $width = $this->icon->get('width');
221
        $height = $this->icon->get('height');
222
        if ($width || $height) {
223
            $this->svgProperties['width'] = $width ?? round($height * $this->svgWidth / $this->svgHeight);
224
            $this->svgProperties['height'] = $height ?? round($width * $this->svgHeight / $this->svgWidth);
225
        }
226
    }
227
228
    /**
229
     * Converts various sizes to pixels.
230
     *
231
     * @param string $size
232
     * @return int
233
     */
234
    private function getPixelValue(string $size): int
235
    {
236
        $trimmedSize = trim($size);
237
        $value = (int) $trimmedSize;
238
        $unit = substr($trimmedSize, -2);
239
240
        if (isset(self::PIXEL_MAP[$unit])) {
241
            $trimmedSize = $value * self::PIXEL_MAP[$unit];
242
        }
243
244
        return (int) round((float) $trimmedSize);
245
    }
246
247
    /**
248
     * Removes nodes from a DOMDocument
249
     *
250
     * @return void
251
     */
252
    private function removeDomNodes(DOMDocument $dom, string $expression): void
253
    {
254
        $xpath = new DOMXPath($dom);
255
        while ($node = $xpath->query($expression)->item(0)) {
256
            if ($node->parentNode) {
257
                $node->parentNode->removeChild($node);
258
            }
259
        }
260
    }
261
262
    /**
263
     * Adds the properties to the SVG.
264
     *
265
     * @return void
266
     */
267
    private function setSvgAttributes(): void
268
    {
269
        $titleElement = $this->svg->createElement('title', $this->icon->getTitle());
270
        $this->svgElement->insertBefore($titleElement, $this->svgElement->firstChild);
271
272
        foreach ($this->svgProperties as $key => $value) {
273
            $this->svgElement->removeAttribute($key);
274
            if (!empty($value)) {
275
                $this->svgElement->setAttribute($key, (string) $value);
276
            }
277
        }
278
    }
279
280
    /**
281
     * Prepares the values to be set on the SVG.
282
     *
283
     * @return void
284
     */
285
    private function setSvgProperties(): void
286
    {
287
        $this->svgProperties['aria-hidden'] = 'true';
288
        $this->svgProperties['role'] = 'img';
289
        $this->svgProperties['id'] = $this->icon->get('id');
290
        $this->svgProperties['class'] = $this->class['class'];
291
292
        $css = $this->icon->get('css');
293
        if (is_array($css)) {
294
            $this->svgProperties['style'] = Html::cssStyleFromArray($css);
295
        }
296
297
        $this->svgProperties['fill'] = $this->icon->get('fill') ?? $this->fill;
298
    }
299
}
300