Completed
Pull Request — 3.x (#127)
by Joschi
02:04
created

Path   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 6
Bugs 0 Features 2
Metric Value
wmc 23
lcom 1
cbo 3
dl 0
loc 247
c 6
b 0
f 2
ccs 85
cts 85
cp 1
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A __invoke() 0 15 2
B getAttributes() 0 25 6
A buildRegex() 0 10 1
A setRegexOptionalAttributes() 0 8 2
A getRegexOptionalAttributesReplacement() 0 12 2
A getRegexOptionalAttributesReplacementHead() 0 11 2
A setRegexAttributes() 0 16 3
A getSubpattern() 0 10 2
A setRegexWildcard() 0 9 2
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) {
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...
158 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...
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) {
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...
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