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
|
|
|
/** |
24
|
|
|
* |
25
|
|
|
* Use this Route to build the regex. |
26
|
|
|
* |
27
|
|
|
* @var Route |
28
|
|
|
* |
29
|
|
|
*/ |
30
|
|
|
protected $route; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* |
34
|
|
|
* The regular expression for the path. |
35
|
|
|
* |
36
|
|
|
* @var string |
37
|
|
|
* |
38
|
|
|
*/ |
39
|
|
|
protected $regex; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* |
43
|
|
|
* The basepath to prefix when matching the path. |
44
|
|
|
* |
45
|
|
|
* @var string |
46
|
|
|
* |
47
|
|
|
*/ |
48
|
|
|
protected $basepath; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* |
52
|
|
|
* Constructor. |
53
|
|
|
* |
54
|
|
|
* @param string $basepath The basepath to prefix when matching the path. |
55
|
|
|
* |
56
|
|
|
*/ |
57
|
14 |
|
public function __construct($basepath = null) |
58
|
|
|
{ |
59
|
14 |
|
$this->basepath = $basepath; |
60
|
14 |
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* |
64
|
|
|
* Checks that the Request path matches the Route path. |
65
|
|
|
* |
66
|
|
|
* @param ServerRequestInterface $request The HTTP request. |
67
|
|
|
* |
68
|
|
|
* @param Route $route The route. |
69
|
|
|
* |
70
|
|
|
* @return bool True on success, false on failure. |
71
|
|
|
* |
72
|
|
|
*/ |
73
|
14 |
|
public function __invoke(ServerRequestInterface $request, Route $route) |
74
|
|
|
{ |
75
|
14 |
|
$match = preg_match( |
76
|
14 |
|
$this->buildRegex($route), |
77
|
14 |
|
$request->getUri()->getPath(), |
78
|
|
|
$matches |
79
|
14 |
|
); |
80
|
|
|
|
81
|
14 |
|
if (! $match) { |
82
|
8 |
|
return false; |
83
|
|
|
} |
84
|
|
|
|
85
|
14 |
|
$route->attributes($this->getAttributes($matches, $route->wildcard)); |
86
|
14 |
|
return true; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* |
91
|
|
|
* Gets the attributes from the path. |
92
|
|
|
* |
93
|
|
|
* @param array $matches The array of matches. |
94
|
|
|
* |
95
|
|
|
* @param string $wildcard The name of the wildcard attributes. |
96
|
|
|
* |
97
|
|
|
* @return array |
98
|
|
|
* |
99
|
|
|
*/ |
100
|
14 |
|
protected function getAttributes($matches, $wildcard) |
101
|
|
|
{ |
102
|
|
|
// if the path match is exactly an empty string, treat it as unset. |
103
|
|
|
// this is to support optional attribute values. |
104
|
14 |
|
$attributes = []; |
105
|
14 |
|
foreach ($matches as $key => $val) { |
106
|
14 |
|
if (is_string($key) && $val !== '') { |
107
|
8 |
|
$attributes[$key] = rawurldecode($val); |
108
|
8 |
|
} |
109
|
14 |
|
} |
110
|
|
|
|
111
|
14 |
|
if (! $wildcard) { |
112
|
13 |
|
return $attributes; |
113
|
|
|
} |
114
|
|
|
|
115
|
1 |
|
$attributes[$wildcard] = []; |
116
|
1 |
|
if (! empty($matches[$wildcard])) { |
117
|
1 |
|
$attributes[$wildcard] = array_map( |
118
|
1 |
|
'rawurldecode', |
119
|
1 |
|
explode('/', $matches[$wildcard]) |
120
|
1 |
|
); |
121
|
1 |
|
} |
122
|
|
|
|
123
|
1 |
|
return $attributes; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* |
128
|
|
|
* Builds the regular expression for the route path. |
129
|
|
|
* |
130
|
|
|
* @param Route $route The Route. |
131
|
|
|
* |
132
|
|
|
* @return string |
133
|
|
|
* |
134
|
|
|
*/ |
135
|
14 |
|
protected function buildRegex(Route $route) |
136
|
|
|
{ |
137
|
14 |
|
$this->route = $route; |
138
|
14 |
|
$this->regex = $this->basepath . $this->route->path; |
139
|
14 |
|
$this->setRegexOptionalAttributes(); |
140
|
14 |
|
$this->setRegexAttributes(); |
141
|
14 |
|
$this->setRegexWildcard(); |
142
|
14 |
|
$this->regex = '#^' . $this->regex . '$#'; |
143
|
14 |
|
return $this->regex; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* |
148
|
|
|
* Expands optional attributes in the regex from ``{/foo,bar,baz}` to |
149
|
|
|
* `(/{foo}(/{bar}(/{baz})?)?)?`. |
150
|
|
|
* |
151
|
|
|
* @return null |
152
|
|
|
* |
153
|
|
|
*/ |
154
|
14 |
|
protected function setRegexOptionalAttributes() |
155
|
|
|
{ |
156
|
14 |
|
preg_match('#{/([a-z][a-zA-Z0-9_,]*)}#', $this->regex, $matches); |
157
|
14 |
|
if ($matches) { |
|
|
|
|
158
|
3 |
|
$repl = $this->getRegexOptionalAttributesReplacement($matches[1]); |
|
|
|
|
159
|
3 |
|
$this->regex = str_replace($matches[0], $repl, $this->regex); |
160
|
3 |
|
} |
161
|
14 |
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* |
165
|
|
|
* Gets the replacement for optional attributes in the regex. |
166
|
|
|
* |
167
|
|
|
* @param array $list The optional attributes. |
168
|
|
|
* |
169
|
|
|
* @return string |
170
|
|
|
* |
171
|
|
|
*/ |
172
|
3 |
|
protected function getRegexOptionalAttributesReplacement($list) |
173
|
|
|
{ |
174
|
3 |
|
$list = explode(',', $list); |
175
|
3 |
|
$head = $this->getRegexOptionalAttributesReplacementHead($list); |
176
|
3 |
|
$tail = ''; |
177
|
3 |
|
foreach ($list as $name) { |
178
|
3 |
|
$head .= "(/{{$name}}"; |
179
|
3 |
|
$tail .= ')?'; |
180
|
3 |
|
} |
181
|
|
|
|
182
|
3 |
|
return $head . $tail; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* |
187
|
|
|
* Gets the leading portion of the optional attributes replacement. |
188
|
|
|
* |
189
|
|
|
* @param array $list The optional attributes. |
190
|
|
|
* |
191
|
|
|
* @return string |
192
|
|
|
* |
193
|
|
|
*/ |
194
|
3 |
|
protected function getRegexOptionalAttributesReplacementHead(&$list) |
195
|
|
|
{ |
196
|
|
|
// if the optional set is the first part of the path, make sure there |
197
|
|
|
// is a leading slash in the replacement before the optional attribute. |
198
|
3 |
|
$head = ''; |
199
|
3 |
|
if (substr($this->regex, 0, 2) == '{/') { |
200
|
2 |
|
$name = array_shift($list); |
201
|
2 |
|
$head = "/({{$name}})?"; |
202
|
2 |
|
} |
203
|
3 |
|
return $head; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* |
208
|
|
|
* Expands attribute names in the regex to named subpatterns; adds default |
209
|
|
|
* `null` values for attributes without defaults. |
210
|
|
|
* |
211
|
|
|
* @return null |
212
|
|
|
* |
213
|
|
|
*/ |
214
|
14 |
|
protected function setRegexAttributes() |
215
|
|
|
{ |
216
|
14 |
|
$find = '#{([a-z][a-zA-Z0-9_]*)}#'; |
217
|
14 |
|
$attributes = $this->route->attributes; |
218
|
14 |
|
$newAttributes = []; |
219
|
14 |
|
preg_match_all($find, $this->regex, $matches, PREG_SET_ORDER); |
220
|
14 |
|
foreach ($matches as $match) { |
|
|
|
|
221
|
8 |
|
$name = $match[1]; |
222
|
8 |
|
$subpattern = $this->getSubpattern($name); |
223
|
8 |
|
$this->regex = str_replace("{{$name}}", $subpattern, $this->regex); |
224
|
8 |
|
if (! isset($attributes[$name])) { |
225
|
8 |
|
$newAttributes[$name] = null; |
226
|
8 |
|
} |
227
|
14 |
|
} |
228
|
14 |
|
$this->route->attributes($newAttributes); |
229
|
14 |
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* |
233
|
|
|
* Returns a named subpattern for a attribute name. |
234
|
|
|
* |
235
|
|
|
* @param string $name The attribute name. |
236
|
|
|
* |
237
|
|
|
* @return string The named subpattern. |
238
|
|
|
* |
239
|
|
|
*/ |
240
|
8 |
|
protected function getSubpattern($name) |
241
|
|
|
{ |
242
|
|
|
// is there a custom subpattern for the name? |
243
|
8 |
|
if (isset($this->route->tokens[$name])) { |
244
|
3 |
|
return "(?P<{$name}>{$this->route->tokens[$name]})"; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
// use a default subpattern |
248
|
6 |
|
return "(?P<{$name}>[^/]+)"; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* |
253
|
|
|
* Adds a wildcard subpattern to the end of the regex. |
254
|
|
|
* |
255
|
|
|
* @return null |
256
|
|
|
* |
257
|
|
|
*/ |
258
|
14 |
|
protected function setRegexWildcard() |
259
|
|
|
{ |
260
|
14 |
|
if (! $this->route->wildcard) { |
261
|
13 |
|
return; |
262
|
|
|
} |
263
|
|
|
|
264
|
1 |
|
$this->regex = rtrim($this->regex, '/') |
265
|
1 |
|
. "(/(?P<{$this->route->wildcard}>.*))?"; |
266
|
1 |
|
} |
267
|
|
|
} |
268
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.