Passed
Push — master ( 590757...67ec3e )
by Divine Niiquaye
02:17
created

Loader   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 67
c 1
b 0
f 0
dl 0
loc 214
ccs 0
cts 73
cp 0
rs 10
wmc 28

11 Methods

Rating   Name   Duplication   Size   Complexity  
A find() 0 11 3
A withExtensions() 0 6 1
A addNamespace() 0 9 2
A exists() 0 20 4
A __construct() 0 3 1
A getFounds() 0 3 1
A findNamespacedView() 0 11 2
A getPossibleFiles() 0 10 1
A parseNamespaceSegments() 0 14 3
B validatePath() 0 23 7
A findInStorage() 0 9 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\UI;
19
20
use Biurad\UI\Exceptions\LoaderException;
21
use Biurad\UI\Interfaces\LoaderInterface;
22
use Biurad\UI\Interfaces\StorageInterface;
23
use Biurad\UI\Storage\CacheStorage;
24
use InvalidArgumentException;
25
26
/**
27
 * Loads and locates view files associated with specific extensions.
28
 */
29
final class Loader implements LoaderInterface
30
{
31
    /** @var array<string,Source> */
32
    protected $founds = [];
33
34
    /** @var string[] */
35
    protected $extensions = [];
36
37
    /** @var StorageInterface */
38
    private $storage;
39
40
    /** @var array<string,mixed> */
41
    private $namespaces = [];
42
43
    /**
44
     * @param StorageInterface $storage
45
     */
46
    public function __construct(StorageInterface $storage)
47
    {
48
        $this->storage = $storage;
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function addNamespace(string $namespace, $hints): void
55
    {
56
        $hints = (array) $hints;
57
58
        if (isset($this->namespaces[$namespace])) {
59
            $hints = \array_merge($this->namespaces[$namespace], $hints);
60
        }
61
62
        $this->namespaces[$namespace] = $hints;
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function withExtensions(array $extensions): LoaderInterface
69
    {
70
        $loader             = clone $this;
71
        $loader->extensions = $extensions;
72
73
        return $loader;
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     *
79
     * @param string $template
80
     */
81
    public function exists(string $view, string &$template = null): bool
82
    {
83
        try {
84
            if (\strpos($view = \trim($view), static::NS_SEPARATOR) > 0) {
85
                $template = $this->findNamespacedView($view);
86
87
                return true;
88
            }
89
90
            $this->validatePath($view);
91
            $template = $this->findInStorage($view);
92
93
            return true;
94
        } catch (LoaderException $e) {
95
            if ("File [{$view}] not found." !== $e->getMessage()) {
96
                throw $e;
97
            }
98
        }
99
100
        return false;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function find(string $view): Source
107
    {
108
        if (isset($this->founds[$view])) {
109
            return $this->founds[$view];
110
        }
111
112
        if (!$this->exists($view, $template)) {
113
            throw new LoaderException("Unable to load view `$view`, file does not exists.");
114
        }
115
116
        return $this->founds[$view] = new Source($template, $this->storage instanceof CacheStorage);
117
    }
118
119
    /**
120
     * Get All found templates served to the user.
121
     *
122
     * @return array<string,Source>
123
     */
124
    public function getFounds(): array
125
    {
126
        return $this->founds;
127
    }
128
129
    /**
130
     * Get the path to a template with a named path.
131
     *
132
     * @param string $name
133
     *
134
     * @return string
135
     */
136
    private function findNamespacedView(string $name): string
137
    {
138
        [$namespace, $view] = $this->parseNamespaceSegments($name);
139
140
        try {
141
            $this->storage->addLocation($this->namespaces[$namespace]);
142
        } catch (LoaderException $e) {
143
            // Do nothing ...
144
        }
145
146
        return $this->findInStorage($view);
147
    }
148
149
    /**
150
     * Find the given view from storage.
151
     *
152
     * @param string $template
153
     *
154
     * @throws LoaderException if template not found
155
     *
156
     * @return string
157
     */
158
    private function findInStorage(string $template): string
159
    {
160
        foreach ($this->getPossibleFiles($template) as $file) {
161
            if (null !== $found = $this->storage->load($file)) {
162
                return $found;
163
            }
164
        }
165
166
        throw new LoaderException("File [{$template}] not found.");
167
    }
168
169
    /**
170
     * Get an array of possible view files.
171
     *
172
     * @param string $name
173
     *
174
     * @return string[]
175
     */
176
    private function getPossibleFiles(string $name): array
177
    {
178
        return \array_map(function ($extension) use ($name): string {
179
            //Cutting extra symbols (see Twig)
180
            return \preg_replace(
181
                '#/{2,}#',
182
                '/',
183
                \str_replace(['\\', '.'], '/', $name)
184
            ) . '.' . $extension;
185
        }, $this->extensions);
186
    }
187
188
    /**
189
     * Get the segments of a template with a named path.
190
     *
191
     * @param string $name
192
     *
193
     * @throws InvalidArgumentException
194
     *
195
     * @return string[]
196
     */
197
    private function parseNamespaceSegments(string $name): array
198
    {
199
        $segments    = \explode(static::NS_SEPARATOR, $name);
200
        $segments[0] = \str_replace(['@', '#'], '', $segments[0]);
201
202
        if (\count($segments) !== 2) {
203
            throw new InvalidArgumentException("View [{$name}] has an invalid name.");
204
        }
205
206
        if (!isset($this->namespaces[$segments[0]])) {
207
            throw new InvalidArgumentException("No hint path defined for [{$segments[0]}].");
208
        }
209
210
        return $segments;
211
    }
212
213
    /**
214
     * Make sure view filename is OK. Same as in twig.
215
     *
216
     * @param string $path
217
     *
218
     * @throws LoaderException
219
     */
220
    private function validatePath(string $path): void
221
    {
222
        if (empty($path)) {
223
            throw new LoaderException('A view path is empty');
224
        }
225
226
        if (false !== \strpos($path, "\0")) {
227
            throw new LoaderException('A view path cannot contain NULL bytes');
228
        }
229
230
        $path  = \ltrim($path, '/');
231
        $parts = \explode('/', $path);
232
        $level = 0;
233
234
        foreach ($parts as $part) {
235
            if ('..' === $part) {
236
                --$level;
237
            } elseif ('.' !== $part) {
238
                ++$level;
239
            }
240
241
            if ($level < 0) {
242
                throw new LoaderException(\sprintf('Looks like you try to load a view outside configured directories (%s)', $path));
243
            }
244
        }
245
    }
246
}
247