Completed
Pull Request — 3.x (#123)
by Hari
05:45 queued 03:43
created

Path::getAttributes()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.972

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 25
ccs 7
cts 10
cp 0.7
rs 8.439
cc 6
eloc 13
nc 9
nop 2
crap 6.972
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 12
    public function __construct($basepath = null)
58
    {
59 12
        $this->basepath = $basepath;
60 4
    }
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 7
    public function __invoke(ServerRequestInterface $request, Route $route)
74
    {
75
        $match = preg_match(
76 3
            $this->buildRegex($route),
77
            $request->getUri()->getPath(),
78
            $matches
79
        );
80
81 3
        if (! $match) {
82 6
            return false;
83
        }
84
85
        $route->attributes($this->getAttributes($matches, $route->wildcard));
86 7
        return true;
87 3
    }
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 7
    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 7
        $attributes = [];
105
        foreach ($matches as $key => $val) {
106
            if (is_string($key) && $val !== '') {
107
                $attributes[$key] = rawurldecode($val);
108
            }
109
        }
110
111 7
        if (! $wildcard) {
112 7
            return $attributes;
113
        }
114
115
        $attributes[$wildcard] = [];
116
        if (! empty($matches[$wildcard])) {
117 1
            $attributes[$wildcard] = array_map(
118 1
                'rawurldecode',
119 1
                explode('/', $matches[$wildcard])
120
            );
121
        }
122
123
        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 3
    protected function buildRegex(Route $route)
136
    {
137 3
        $this->route = $route;
138
        $this->regex = $this->basepath . $this->route->path;
139
        $this->setRegexOptionalAttributes();
140
        $this->setRegexAttributes();
141
        $this->setRegexWildcard();
142 3
        $this->regex = '#^' . $this->regex . '$#';
143 3
        return $this->regex;
144 3
    }
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 3
    protected function setRegexOptionalAttributes()
155
    {
156
        preg_match('#{/([a-z][a-zA-Z0-9_,]*)}#', $this->regex, $matches);
157 3
        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
            $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
            $this->regex = str_replace($matches[0], $repl, $this->regex);
160
        }
161 3
    }
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
    protected function getRegexOptionalAttributesReplacement($list)
173
    {
174
        $list = explode(',', $list);
175
        $head = $this->getRegexOptionalAttributesReplacementHead($list);
176
        $tail = '';
177
        foreach ($list as $name) {
178
            $head .= "(/{{$name}}";
179
            $tail .= ')?';
180
        }
181
182
        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
    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
        $head = '';
199
        if (substr($this->regex, 0, 2) == '{/') {
200
            $name = array_shift($list);
201
            $head = "/({{$name}})?";
202
        }
203
        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 3
    protected function setRegexAttributes()
215
    {
216 3
        $find = '#{([a-z][a-zA-Z0-9_]*)}#';
217
        $attributes = $this->route->attributes;
218 3
        $newAttributes = [];
219
        preg_match_all($find, $this->regex, $matches, PREG_SET_ORDER);
220
        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
            $name = $match[1];
222
            $subpattern = $this->getSubpattern($name);
223
            $this->regex = str_replace("{{$name}}", $subpattern, $this->regex);
224
            if (! isset($attributes[$name])) {
225
                $newAttributes[$name] = null;
226
            }
227
        }
228
        $this->route->attributes($newAttributes);
229 3
    }
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 1
    protected function getSubpattern($name)
241
    {
242
        // is there a custom subpattern for the name?
243
        if (isset($this->route->tokens[$name])) {
244
            return "(?P<{$name}>{$this->route->tokens[$name]})";
245
        }
246
247
        // use a default subpattern
248 1
        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 3
    protected function setRegexWildcard()
259
    {
260
        if (! $this->route->wildcard) {
261 3
            return;
262
        }
263
264
        $this->regex = rtrim($this->regex, '/')
265
                     . "(/(?P<{$this->route->wildcard}>.*))?";
266
    }
267
}
268