Passed
Pull Request — 3.x (#196)
by
unknown
01:27
created

Path   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 33
eloc 81
c 3
b 1
f 0
dl 0
loc 276
ccs 81
cts 81
cp 1
rs 9.76

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setRegexOptionalAttributes() 0 6 2
A buildRegex() 0 9 1
A getAttributes() 0 24 6
A __construct() 0 3 1
A getRegexOptionalAttributesReplacement() 0 11 2
A __invoke() 0 24 6
A getSubpattern() 0 14 5
A getRegexOptionalAttributesReplacementList() 0 9 2
A getRegexOptionalAttributesReplacementHead() 0 10 2
A setRegexWildcard() 0 8 2
A setRegexAttributes() 0 15 4
1
<?php
2
/**
3
 *
4
 * This file is part of Aura for PHP.
5
 *
6
 * @license http://opensource.org/licenses/bsd-license.php BSD
7
 *
8
 */
9
namespace Aura\Router\Rule;
10
11
use Aura\Router\Route;
12
use Psr\Http\Message\ServerRequestInterface;
13
14
/**
15
 *
16
 * A rule for the URL path.
17
 *
18
 * @package Aura.Router
19
 *
20
 */
21
class Path implements RuleInterface
22
{
23
    const REGEX = '#{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*:*\s*([^{}]*{*[^{}]*}*[^{}]*)\s*}#';
24
    const OPT_REGEX = '#{\s*/\s*([a-z][a-zA-Z0-9_-]*\s*:*\s*[^/]*{*[^/]*}*[^/]*,*)}#';
25
    const SPLIT_REGEX = '#\s*,\s*(?![^{]*})#';
26
27
    /**
28
     *
29
     * Use this Route to build the regex.
30
     *
31
     * @var Route
32
     *
33
     */
34
    protected $route;
35
36
    /**
37
     *
38
     * The regular expression for the path.
39
     *
40
     * @var string
41
     *
42
     */
43
    protected $regex;
44
45
    /**
46
     *
47
     * The basepath to prefix when matching the path.
48
     *
49
     * @var string|null
50
     *
51
     */
52
    protected $basepath;
53
54
    /**
55
     *
56
     * Constructor.
57 15
     *
58
     * @param string $basepath The basepath to prefix when matching the path.
59 15
     *
60 15
     */
61
    public function __construct($basepath = null)
62
    {
63
        $this->basepath = $basepath;
64
    }
65
66
    /**
67
     *
68
     * Checks that the Request path matches the Route path.
69
     *
70
     * @param ServerRequestInterface $request The HTTP request.
71
     *
72
     * @param Route $route The route.
73 15
     *
74
     * @return bool True on success, false on failure.
75 15
     *
76 15
     */
77 15
    public function __invoke(ServerRequestInterface $request, Route $route)
78 15
    {
79
        $match = preg_match(
80
            $this->buildRegex($route),
81 15
            $request->getUri()->getPath(),
82 8
            $matches
83
        );
84
85 15
        if (! $match) {
86
            return false;
87 15
        }
88 4
89 1
        $attributes = $this->getAttributes($matches, $route->wildcard);
90 4
91
        foreach ($this->route->tokens as $name => $pattern) {
92
            if (is_callable($pattern)) {
93
                if (!$pattern(isset($attributes[$name]) ? $attributes[$name] : null, $route, $request)) {
94
                    return false;
95 15
                }
96 15
            }
97
        }
98
99
        $route->attributes($attributes);
100
        return true;
101
    }
102
103
    /**
104
     *
105
     * Gets the attributes from the path.
106
     *
107
     * @param array $matches The array of matches.
108
     *
109
     * @param string $wildcard The name of the wildcard attributes.
110 15
     *
111
     * @return array
112
     *
113
     */
114 15
    protected function getAttributes($matches, $wildcard)
115 15
    {
116 15
        // if the path match is exactly an empty string, treat it as unset.
117 15
        // this is to support optional attribute values.
118
        $attributes = [];
119
        foreach ($matches as $key => $val) {
120
            if (is_string($key) && $val !== '') {
121 15
                $attributes[$key] = rawurldecode($val);
122 14
            }
123
        }
124
125 1
        if (! $wildcard) {
126 1
            return $attributes;
127 1
        }
128 1
129 1
        $attributes[$wildcard] = [];
130
        if (! empty($matches[$wildcard])) {
131
            $attributes[$wildcard] = array_map(
132
                'rawurldecode',
133 1
                explode('/', $matches[$wildcard])
134
            );
135
        }
136
137
        return $attributes;
138
    }
139
140
    /**
141
     *
142
     * Builds the regular expression for the route path.
143
     *
144
     * @param Route $route The Route.
145 15
     *
146
     * @return string
147 15
     *
148 15
     */
149 15
    protected function buildRegex(Route $route)
150 15
    {
151 15
        $this->route = $route;
152 15
        $this->regex = $this->basepath . $this->route->path;
153 15
        $this->setRegexOptionalAttributes();
154
        $this->setRegexAttributes();
155
        $this->setRegexWildcard();
156
        $this->regex = '#^' . $this->regex . '$#';
157
        return $this->regex;
158
    }
159
160
    /**
161
     *
162
     * Expands optional attributes in the regex from ``{/foo,bar,baz}` to
163
     * `(/{foo}(/{bar}(/{baz})?)?)?`.
164 15
     *
165
     */
166 15
    protected function setRegexOptionalAttributes()
167 15
    {
168 3
        preg_match(self::OPT_REGEX, $this->regex, $matches);
169 3
        if ($matches) {
170
            $repl = $this->getRegexOptionalAttributesReplacement($matches[1]);
171 15
            $this->regex = str_replace($matches[0], $repl, $this->regex);
172
        }
173
    }
174
175
    /**
176
     *
177
     * Gets the replacement for optional attributes in the regex.
178
     *
179
     * @param string $list The optional attributes.
180
     *
181
     * @return string
182 3
     *
183
     */
184 3
    protected function getRegexOptionalAttributesReplacement($list)
185 3
    {
186 3
        $list = $this->getRegexOptionalAttributesReplacementList($list);
187 3
        $head = $this->getRegexOptionalAttributesReplacementHead($list);
188 3
        $tail = '';
189 3
        foreach ($list as $name) {
190
            $head .= "(/{{$name}}";
191
            $tail .= ')?';
192 3
        }
193
194
        return $head . $tail;
195
    }
196
197
    /**
198
     * Get list of optional attributes from regex
199
     *
200
     * @param $list
201
     * @return string[]
202
     */
203
    protected function getRegexOptionalAttributesReplacementList($list)
204 3
    {
205
        $list = trim($list);
206
        $newList = preg_split(self::SPLIT_REGEX, $list);
207
        if (false === $newList) {
208 3
            return [$list];
209 3
        }
210 2
211 2
        return $newList;
212
    }
213 3
214
    /**
215
     *
216
     * Gets the leading portion of the optional attributes replacement.
217
     *
218
     * @param array $list The optional attributes.
219
     *
220
     * @return string
221
     *
222
     */
223
    protected function getRegexOptionalAttributesReplacementHead(&$list)
224 15
    {
225
        // if the optional set is the first part of the path, make sure there
226 15
        // is a leading slash in the replacement before the optional attribute.
227 15
        $head = '';
228 15
        if (substr($this->regex, 0, 2) == '{/') {
229 15
            $name = array_shift($list);
230 15
            $head = "/({{$name}})?";
231 9
        }
232 9
        return $head;
233 9
    }
234 9
235 9
    /**
236
     *
237
     * Expands attribute names in the regex to named subpatterns; adds default
238 15
     * `null` values for attributes without defaults.
239 15
     *
240
     */
241
    protected function setRegexAttributes()
242
    {
243
        $attributes = $this->route->attributes;
244
        $newAttributes = [];
245
        preg_match_all(self::REGEX, $this->regex, $matches, PREG_SET_ORDER);
246
        foreach ($matches as $match) {
247
            $name = $match[1];
248
            $token = isset($match[2]) ? $match[2] : null;
249
            $subpattern = $this->getSubpattern($name, $token);
250 9
            $this->regex = str_replace($match[0], $subpattern, $this->regex);
251
            if (! isset($attributes[$name])) {
252
                $newAttributes[$name] = null;
253 9
            }
254 4
        }
255
        $this->route->attributes($newAttributes);
256
    }
257
258 7
    /**
259
     *
260
     * Returns a named subpattern for an attribute name.
261
     *
262
     * @param string $name The attribute name.
263
     * @param string|null $token The pattern FastRoute format from route
264
     *
265
     * @return string The named subpattern.
266
     *
267
     */
268 15
    protected function getSubpattern($name, $token = null)
269
    {
270 15
        // is there a custom subpattern for the name?
271 14
        if (isset($this->route->tokens[$name]) && is_string($this->route->tokens[$name])) {
272
            // if $token is null use route token
273
            $token = $token ?: $this->route->tokens[$name];
274 1
        }
275 1
276 1
        if ($token) {
277
            return "(?P<{$name}>{$token})";
278
        }
279
280
        // use a default subpattern
281
        return "(?P<{$name}>[^/]+)";
282
    }
283
284
    /**
285
     *
286
     * Adds a wildcard subpattern to the end of the regex.
287
     *
288
     */
289
    protected function setRegexWildcard()
290
    {
291
        if (! $this->route->wildcard) {
292
            return;
293
        }
294
295
        $this->regex = rtrim($this->regex, '/')
296
                     . "(/(?P<{$this->route->wildcard}>.*))?";
297
    }
298
}
299