IconifyLoader::configureOptions()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
dl 0
loc 13
ccs 10
cts 10
cp 1
rs 10
c 1
b 0
f 0
cc 2
nc 1
nop 1
crap 2
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\Provider\Iconify;
13
14
use Iconify\JSONTools\Collection;
15
use Iconify\JSONTools\SVG as Icon;
16
use Ocubom\Twig\Extension\Svg\Exception\JsonException;
17
use Ocubom\Twig\Extension\Svg\Exception\LoaderException;
18
use Ocubom\Twig\Extension\Svg\Exception\LogicException;
19
use Ocubom\Twig\Extension\Svg\Loader\LoaderInterface;
20
use Ocubom\Twig\Extension\Svg\Svg;
21
use Ocubom\Twig\Extension\Svg\Util\PathCollection;
22
use Symfony\Component\Filesystem\Filesystem;
23
use Symfony\Component\Filesystem\Path;
24
use Symfony\Component\OptionsResolver\Options;
25
use Symfony\Component\OptionsResolver\OptionsResolver;
26
27
use function BenTools\IterableFunctions\iterable_to_array;
28
use function Ocubom\Twig\Extension\is_string;
29
30
class IconifyLoader implements LoaderInterface
31
{
32
    private PathCollection $searchPath;
33
    private ?string $cacheDir;
34
    private static Filesystem $fs;
35
    private const CACHE_BASEDIR = 'iconify';
36
    private const CACHE_EXTENSION = 'php';
37
38 20
    public function __construct(PathCollection $searchPath, iterable $options = null)
39
    {
40 20
        self::$fs = new Filesystem();
41
42 20
        $this->searchPath = $searchPath;
43
44
        // Parse options
45 20
        $options = static::configureOptions()
46 20
            ->resolve(iterable_to_array($options ?? /* @scrutinizer ignore-type */ []));
47
48
        // Check cache directory path
49 20
        $this->cacheDir = Path::canonicalize($options['cache_dir'] ?? '');
50 20
        if ('' === $this->cacheDir) {
51
            $this->cacheDir = null; // @codeCoverageIgnore
52
        } else {
53
            // Ensure cache dir is inside prefixed subdir
54 20
            if (self::CACHE_BASEDIR !== Path::getFilenameWithoutExtension($this->cacheDir)) {
55 20
                $this->cacheDir = Path::join($this->cacheDir, self::CACHE_BASEDIR);
56
            }
57
        }
58
    }
59
60 9
    public function resolve(string $ident, iterable $options = null): Svg
61
    {
62
        // Split ident
63 9
        $tokens = preg_split('@[/:\-]+@Uis', $ident);
64 9
        $count = count($tokens);
65 9
        for ($idx = 1; $idx < $count; ++$idx) {
66 9
            $icon = $this->loadIcon(
67 9
                join('-', array_slice($tokens, 0, $idx)),
68 9
                join('-', array_slice($tokens, $idx - $count))
69 9
            );
70
71 8
            if ($icon) {
72 5
                $options = Icon::splitAttributes(iterable_to_array($options ?? []));
73
74 5
                return new IconifySvg(
75 5
                    $icon->getSVG($options['icon']),
76 5
                    $options['node']
77 5
                );
78
            }
79
        }
80
81 3
        throw new LoaderException($ident, new \ReflectionClass($this));
82
    }
83
84 9
    private function loadIcon(string $prefix, string $name): ?Icon
85
    {
86 9
        foreach ($this->getCollectionPaths($prefix) as $path) {
87 9
            if (!self::$fs->exists($path)) {
88 4
                continue;
89
            }
90
91 7
            $collection = new Collection();
92 7
            if (!$collection->loadFromFile($path, null, $this->getCacheFile($prefix))) {
93 1
                throw new JsonException(sprintf('Unable to parse "%s" JSON file', $path));
94
            }
95
96 6
            $data = $collection->getIconData($name);
97 6
            if (!$data) {
98 1
                continue;
99
            }
100
101 5
            return new Icon($data);
102
        }
103
104
        // Unable to find icon
105 3
        return null;
106
    }
107
108 7
    private function getCacheFile(string $collection): ?string
109
    {
110 7
        if (null === $this->cacheDir) {
111
            return null; // @codeCoverageIgnore
112
        }
113
114 7
        $path = Path::changeExtension(
115 7
            Path::join($this->cacheDir, $collection),
116 7
            self::CACHE_EXTENSION
117 7
        );
118 7
        if (!Path::isBasePath($this->cacheDir, $path)) {
119
            // @codeCoverageIgnoreStart
120
            throw new LogicException(sprintf(
121
                'The generated cache path `%s` is outside the base cache directory `%s`.',
122
                $path,
123
                $this->cacheDir
124
            ));
125
            // @codeCoverageIgnoreEnd
126
        }
127
128
        // Ensure cache dir exists
129 7
        self::$fs->mkdir($this->cacheDir, 0755);
130
131 7
        return $path;
132
    }
133
134
    /**
135
     * @psalm-return \Generator<int, string, mixed, void>
136
     */
137 9
    private function getCollectionPaths(string $name): \Generator
138
    {
139 9
        foreach ($this->searchPath as $basePath) {
140
            // Try @iconify/json path (full set)
141 9
            yield Path::join((string) $basePath, 'json', $name.'.json');
142
143
            // Try @iconify-json path (cherry picking)
144 4
            yield Path::join((string) $basePath, $name, 'icons.json');
145
        }
146
    }
147
148
    /** @psalm-suppress MissingClosureParamType */
149 20
    protected static function configureOptions(OptionsResolver $resolver = null): OptionsResolver
150
    {
151 20
        $resolver = $resolver ?? new OptionsResolver();
152
153 20
        $resolver->define('cache_dir')
154 20
            ->default(null)
155 20
            ->allowedTypes('null', 'string', \SplFileInfo::class)
156 20
            ->normalize(function (Options $options, $value): string {
157 20
                return is_string($value) ? $value : ($value ?? '');
158 20
            })
159 20
            ->info('Where cache files will be stored');
160
161 20
        return $resolver;
162
    }
163
}
164