Completed
Push — 3.x ( f7e54e...905f8c )
by Paul
10:37
created

Path::setRegexAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
ccs 15
cts 15
cp 1
rs 9.4285
cc 3
eloc 12
nc 3
nop 0
crap 3
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 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
            $matches
79 15
        );
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
                    return false;
91
                }
92 1
            }
93 15
        }
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 9
                $attributes[$key] = rawurldecode($val);
118 9
            }
119 15
        }
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 1
            );
131 1
        }
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
168 3
            $repl = $this->getRegexOptionalAttributesReplacement($matches[1]);
0 ignored issues
show
Documentation introduced by
$matches[1] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
169 3
            $this->regex = str_replace($matches[0], $repl, $this->regex);
170 3
        }
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);
185 3
        $head = $this->getRegexOptionalAttributesReplacementHead($list);
186 3
        $tail = '';
187 3
        foreach ($list as $name) {
188 3
            $head .= "(/{{$name}}";
189 3
            $tail .= ')?';
190 3
        }
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 2
        }
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) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 9
            }
237 15
        }
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