Passed
Pull Request — 3.x (#177)
by Akihito
01:17
created

Path::getAttributes()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
nc 9
nop 2
dl 0
loc 24
ccs 13
cts 13
cp 1
crap 6
rs 9.2222
c 1
b 0
f 0
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|null
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 15
    public function __construct($basepath = null)
58
    {
59 15
        $this->basepath = $basepath;
60 15
    }
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 15
    public function __invoke(ServerRequestInterface $request, Route $route)
74
    {
75 15
        $match = preg_match(
76 15
            $this->buildRegex($route),
77 15
            $request->getUri()->getPath(),
78 15
            $matches
79
        );
80
81 15
        if (! $match) {
82 8
            return false;
83
        }
84
85 15
        $attributes = $this->getAttributes($matches, $route->wildcard);
86
87 15
        foreach ($this->route->tokens as $name => $pattern) {
88 4
            if (is_callable($pattern)) {
89 1
                if (!$pattern($attributes[$name], $route, $request)) {
90 4
                    return false;
91
                }
92
            }
93
        }
94
95 15
        $route->attributes($attributes);
96 15
        return true;
97
    }
98
99
    /**
100
     *
101
     * Gets the attributes from the path.
102
     *
103
     * @param array $matches The array of matches.
104
     *
105
     * @param string $wildcard The name of the wildcard attributes.
106
     *
107
     * @return array
108
     *
109
     */
110 15
    protected function getAttributes($matches, $wildcard)
111
    {
112
        // if the path match is exactly an empty string, treat it as unset.
113
        // this is to support optional attribute values.
114 15
        $attributes = [];
115 15
        foreach ($matches as $key => $val) {
116 15
            if (is_string($key) && $val !== '') {
117 15
                $attributes[$key] = rawurldecode($val);
118
            }
119
        }
120
121 15
        if (! $wildcard) {
122 14
            return $attributes;
123
        }
124
125 1
        $attributes[$wildcard] = [];
126 1
        if (! empty($matches[$wildcard])) {
127 1
            $attributes[$wildcard] = array_map(
128 1
                'rawurldecode',
129 1
                explode('/', $matches[$wildcard])
130
            );
131
        }
132
133 1
        return $attributes;
134
    }
135
136
    /**
137
     *
138
     * Builds the regular expression for the route path.
139
     *
140
     * @param Route $route The Route.
141
     *
142
     * @return string
143
     *
144
     */
145 15
    protected function buildRegex(Route $route)
146
    {
147 15
        $this->route = $route;
148 15
        $this->regex = $this->basepath . $this->route->path;
149 15
        $this->setRegexOptionalAttributes();
150 15
        $this->setRegexAttributes();
151 15
        $this->setRegexWildcard();
152 15
        $this->regex = '#^' . $this->regex . '$#';
153 15
        return $this->regex;
154
    }
155
156
    /**
157
     *
158
     * Expands optional attributes in the regex from ``{/foo,bar,baz}` to
159
     * `(/{foo}(/{bar}(/{baz})?)?)?`.
160
     *
161
     * @return null
162
     *
163
     */
164 15
    protected function setRegexOptionalAttributes()
165
    {
166 15
        preg_match('#{/([a-z][a-zA-Z0-9_,]*)}#', $this->regex, $matches);
167 15
        if ($matches) {
168 3
            $repl = $this->getRegexOptionalAttributesReplacement($matches[1]);
169 3
            $this->regex = str_replace($matches[0], $repl, $this->regex);
170
        }
171 15
    }
172
173
    /**
174
     *
175
     * Gets the replacement for optional attributes in the regex.
176
     *
177
     * @param array $list The optional attributes.
178
     *
179
     * @return string
180
     *
181
     */
182 3
    protected function getRegexOptionalAttributesReplacement($list)
183
    {
184 3
        $list = explode(',', $list);
0 ignored issues
show
Bug introduced by
$list of type array is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
        $list = explode(',', /** @scrutinizer ignore-type */ $list);
Loading history...
185 3
        $head = $this->getRegexOptionalAttributesReplacementHead($list);
186 3
        $tail = '';
187 3
        foreach ($list as $name) {
188 3
            $head .= "(/{{$name}}";
189 3
            $tail .= ')?';
190
        }
191
192 3
        return $head . $tail;
193
    }
194
195
    /**
196
     *
197
     * Gets the leading portion of the optional attributes replacement.
198
     *
199
     * @param array $list The optional attributes.
200
     *
201
     * @return string
202
     *
203
     */
204 3
    protected function getRegexOptionalAttributesReplacementHead(&$list)
205
    {
206
        // if the optional set is the first part of the path, make sure there
207
        // is a leading slash in the replacement before the optional attribute.
208 3
        $head = '';
209 3
        if (substr($this->regex, 0, 2) == '{/') {
210 2
            $name = array_shift($list);
211 2
            $head = "/({{$name}})?";
212
        }
213 3
        return $head;
214
    }
215
216
    /**
217
     *
218
     * Expands attribute names in the regex to named subpatterns; adds default
219
     * `null` values for attributes without defaults.
220
     *
221
     * @return null
222
     *
223
     */
224 15
    protected function setRegexAttributes()
225
    {
226 15
        $find = '#{([a-z][a-zA-Z0-9_]*)}#';
227 15
        $attributes = $this->route->attributes;
228 15
        $newAttributes = [];
229 15
        preg_match_all($find, $this->regex, $matches, PREG_SET_ORDER);
230 15
        foreach ($matches as $match) {
231 9
            $name = $match[1];
232 9
            $subpattern = $this->getSubpattern($name);
233 9
            $this->regex = str_replace("{{$name}}", $subpattern, $this->regex);
234 9
            if (! isset($attributes[$name])) {
235 9
                $newAttributes[$name] = null;
236
            }
237
        }
238 15
        $this->route->attributes($newAttributes);
239 15
    }
240
241
    /**
242
     *
243
     * Returns a named subpattern for a attribute name.
244
     *
245
     * @param string $name The attribute name.
246
     *
247
     * @return string The named subpattern.
248
     *
249
     */
250 9
    protected function getSubpattern($name)
251
    {
252
        // is there a custom subpattern for the name?
253 9
        if (isset($this->route->tokens[$name]) && is_string($this->route->tokens[$name])) {
254 4
            return "(?P<{$name}>{$this->route->tokens[$name]})";
255
        }
256
257
        // use a default subpattern
258 7
        return "(?P<{$name}>[^/]+)";
259
    }
260
261
    /**
262
     *
263
     * Adds a wildcard subpattern to the end of the regex.
264
     *
265
     * @return null
266
     *
267
     */
268 15
    protected function setRegexWildcard()
269
    {
270 15
        if (! $this->route->wildcard) {
271 14
            return;
272
        }
273
274 1
        $this->regex = rtrim($this->regex, '/')
275 1
                     . "(/(?P<{$this->route->wildcard}>.*))?";
276 1
    }
277
}
278