Completed
Push — master ( 32c171...66d5a1 )
by Anton
02:51
created

ViewLoader::getSource()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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