Passed
Push — master ( 72c793...7f699f )
by Kirill
03:22
created

PathParser::parse()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 38
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 22
c 1
b 0
f 0
nc 13
nop 1
dl 0
loc 38
rs 8.6346
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Views\Loader;
13
14
use Spiral\Views\Exception\PathException;
15
use Spiral\Views\LoaderInterface;
16
17
/**
18
 * Parse view path and return name chunks (namespace, name, basename).
19
 */
20
final class PathParser
21
{
22
    /** @var string */
23
    private $defaultNamespace;
24
25
    /** @var string|null */
26
    private $extension;
27
28
    /**
29
     * @param string $defaultNamespace
30
     * @param string $extension
31
     */
32
    public function __construct(string $defaultNamespace, string $extension)
33
    {
34
        $this->defaultNamespace = $defaultNamespace;
35
        $this->extension = $extension;
36
    }
37
38
    /**
39
     * @return string
40
     */
41
    public function getExtension(): string
42
    {
43
        return $this->extension;
44
    }
45
46
    /**
47
     * Check if filename matches to expected extension.
48
     *
49
     * @param string $filename
50
     * @return bool
51
     */
52
    public function match(string $filename): bool
53
    {
54
        $extension = substr($filename, -strlen($this->extension) - 1);
55
        return strtolower($extension) === ".{$this->extension}";
56
    }
57
58
    /**
59
     * Parse view path and extract name, namespace and basename information.
60
     *
61
     * @param string $path
62
     * @return null|ViewPath
63
     *
64
     * @throws PathException
65
     */
66
    public function parse(string $path): ?ViewPath
67
    {
68
        $this->validatePath($path);
69
70
        //Cutting extra symbols (see Twig)
71
        $filename = preg_replace(
72
            '#/{2,}#',
73
            '/',
74
            str_replace('\\', '/', (string)$path)
75
        );
76
77
        $namespace = $this->defaultNamespace;
78
        if (strpos($filename, '.') === false) {
79
            //Force default extension
80
            $filename .= '.' . $this->extension;
81
        } elseif (!$this->match($filename)) {
82
            return null;
83
        }
84
85
        if (strpos($filename, LoaderInterface::NS_SEPARATOR) !== false) {
86
            list($namespace, $filename) = explode(LoaderInterface::NS_SEPARATOR, $filename);
87
        }
88
89
        //Twig like namespaces
90
        if (isset($filename[0]) && $filename[0] == '@') {
91
            $separator = strpos($filename, '/');
92
            if ($separator === false) {
93
                throw new PathException(sprintf('Malformed view path"%s" (expecting "@namespace/name").', $path));
94
            }
95
96
            $namespace = substr($filename, 1, $separator - 1);
97
            $filename = substr($filename, $separator + 1);
98
        }
99
100
        return new ViewPath(
101
            $namespace,
102
            $this->fetchName($filename),
103
            $filename
104
        );
105
    }
106
107
    /**
108
     * Get view name from given filename.
109
     *
110
     * @param string $filename
111
     * @return null|string
112
     */
113
    public function fetchName(string $filename): ?string
114
    {
115
        return str_replace('\\', '/', substr($filename, 0, -1 * (1 + strlen($this->extension))));
116
    }
117
118
    /**
119
     * Make sure view filename is OK. Same as in twig.
120
     *
121
     * @param string $path
122
     * @throws PathException
123
     */
124
    private function validatePath(string $path): void
125
    {
126
        if (empty($path)) {
127
            throw new PathException('A view path is empty');
128
        }
129
130
        if (false !== strpos($path, "\0")) {
131
            throw new PathException('A view path cannot contain NUL bytes');
132
        }
133
134
        $path = ltrim($path, '/');
135
        $parts = explode('/', $path);
136
        $level = 0;
137
138
        foreach ($parts as $part) {
139
            if ('..' === $part) {
140
                --$level;
141
            } elseif ('.' !== $part) {
142
                ++$level;
143
            }
144
145
            if ($level < 0) {
146
                throw new PathException(sprintf(
147
                    'Looks like you try to load a view outside configured directories (%s)',
148
                    $path
149
                ));
150
            }
151
        }
152
    }
153
}
154