Completed
Branch 09branch (31301e)
by Anton
02:42
created

FileLoader   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 219
rs 10
c 0
b 0
f 0
wmc 27
lcom 1
cbo 5

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getNamespaces() 0 4 1
A withExtension() 0 7 1
B getSource() 0 33 5
A exists() 0 13 3
C parsePath() 0 36 7
B validatePath() 0 24 6
A flushCache() 0 6 1
A fetchName() 0 8 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Views\Loaders;
10
11
use Spiral\Core\Component;
12
use Spiral\Debug\Traits\BenchmarkTrait;
13
use Spiral\Files\FilesInterface;
14
use Spiral\Views\Exceptions\LoaderException;
15
use Spiral\Views\LoaderInterface;
16
use Spiral\Views\SourceContextInterface;
17
use Spiral\Views\ViewsInterface;
18
use Spiral\Views\ViewSource;
19
20
/**
21
 * Default views loader is very similar to twig loader (compatible), however it uses different view
22
 * namespace syntax, can change it's default namespace and force specified file extension. Plus it
23
 * works over FilesInterface.
24
 */
25
class FileLoader extends Component implements LoaderInterface
26
{
27
    use BenchmarkTrait;
28
29
    /**
30
     * View cache. Can be improved using MemoryInterface.
31
     *
32
     * @var array
33
     */
34
    private $sourceCache = [];
35
36
    /**
37
     * Such extensions will automatically be added to every file but only if no other extension
38
     * specified in view name. As result you are able to render "home" view, instead of "home.twig".
39
     *
40
     * @var string|null
41
     */
42
    protected $extension = null;
43
44
    /**
45
     * Available view namespaces associated with their directories.
46
     *
47
     * @var array
48
     */
49
    protected $namespaces = [];
50
51
    /**
52
     * @var FilesInterface
53
     */
54
    protected $files = null;
55
56
    /**
57
     * @param array          $namespaces
58
     * @param FilesInterface $files
59
     */
60
    public function __construct(array $namespaces, FilesInterface $files)
61
    {
62
        $this->namespaces = $namespaces;
63
        $this->files = $files;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getNamespaces(): array
70
    {
71
        return $this->namespaces;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function withExtension(string $extension = null): LoaderInterface
78
    {
79
        $loader = clone $this;
80
        $loader->extension = $extension;
81
82
        return $loader->flushCache();
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function getSource(string $path): ViewSource
89
    {
90
        if (isset($this->sourceCache[$path])) {
91
            //Already resolved and cached
92
            return $this->sourceCache[$path];
93
        }
94
95
        //Making sure requested name is valid
96
        $this->validatePath($path);
97
98
        list($namespace, $filename) = $this->parsePath($path);
99
100
        if (!isset($this->namespaces[$namespace])) {
101
            throw new LoaderException("Undefined view namespace '{$namespace}'");
102
        }
103
104
        foreach ($this->namespaces[$namespace] as $directory) {
105
            //Seeking for view filename
106
            if ($this->files->exists($directory . $filename)) {
107
108
                //Found view context
109
                $this->sourceCache[$path] = new ViewSource(
110
                    $directory . $filename,
111
                    $this->fetchName($filename),
112
                    $namespace
113
                );
114
115
                return $this->sourceCache[$path];
116
            }
117
        }
118
119
        throw new LoaderException("Unable to locate view '{$filename}' in namespace '{$namespace}'");
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function exists(string $path): bool
126
    {
127
        if (isset($this->sourceCache[$path])) {
128
            //Already resolved and cached
129
            return true;
130
        }
131
132
        try {
133
            return !empty($this->getSource($path));
134
        } catch (LoaderException $e) {
135
            return false;
136
        }
137
    }
138
139
    /**
140
     * Fetch namespace and filename from view name or force default values.
141
     *
142
     * @param string $path
143
     *
144
     * @return array
145
     * @throws LoaderException
146
     */
147
    protected function parsePath(string $path): array
148
    {
149
        //Cutting extra symbols (see Twig)
150
        $filename = preg_replace(
151
            '#/{2,}#',
152
            '/',
153
            str_replace('\\', '/', (string)$path)
154
        );
155
156
        if (strpos($filename, '.') === false && !empty($this->extension)) {
157
            //Forcing default extension
158
            $filename .= '.' . $this->extension;
159
        }
160
161
        if (strpos($filename, ViewsInterface::NS_SEPARATOR) !== false) {
162
            return explode(ViewsInterface::NS_SEPARATOR, $filename);
163
        }
164
165
        //Twig like namespaces
166
        if (isset($filename[0]) && $filename[0] == '@') {
167
            if (($separator = strpos($filename, '/')) === false) {
168
                throw new LoaderException(sprintf(
169
                    'Malformed namespaced template name "%s" (expecting "@namespace/template_name").',
170
                    $path
171
                ));
172
            }
173
174
            $namespace = substr($filename, 1, $separator - 1);
175
            $filename = substr($filename, $separator + 1);
176
177
            return [$namespace, $filename];
178
        }
179
180
        //Let's force default namespace
181
        return [ViewsInterface::DEFAULT_NAMESPACE, $filename];
182
    }
183
184
    /**
185
     * Make sure view filename is OK. Same as in twig.
186
     *
187
     * @param string $name
188
     *
189
     * @throws LoaderException
190
     */
191
    protected function validatePath(string $name)
192
    {
193
        if (false !== strpos($name, "\0")) {
194
            throw new LoaderException('A template name cannot contain NUL bytes');
195
        }
196
197
        $name = ltrim($name, '/');
198
        $parts = explode('/', $name);
199
        $level = 0;
200
        foreach ($parts as $part) {
201
            if ('..' === $part) {
202
                --$level;
203
            } elseif ('.' !== $part) {
204
                ++$level;
205
            }
206
207
            if ($level < 0) {
208
                throw new LoaderException(sprintf(
209
                    'Looks like you try to load a template outside configured directories (%s)',
210
                    $name
211
                ));
212
            }
213
        }
214
    }
215
216
    /**
217
     * Flushing loading cache.
218
     *
219
     * @return self
220
     */
221
    protected function flushCache(): FileLoader
222
    {
223
        $this->sourceCache = [];
224
225
        return $this;
226
    }
227
228
    /**
229
     * Resolve view name based on filename (depends on current extension settings).
230
     *
231
     * @param string $filename
232
     *
233
     * @return string
234
     */
235
    private function fetchName(string $filename): string
236
    {
237
        if (empty($this->extension)) {
238
            return $filename;
239
        }
240
241
        return substr($filename, 0, -1 * (1 + strlen($this->extension)));
242
    }
243
}