Test Failed
Push — master ( d89d94...06c219 )
by Mikael
01:52 queued 11s
created

Route::matchPart()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 6
cts 6
cp 1
rs 9.2
cc 4
eloc 14
nc 6
nop 3
crap 4
1
<?php
2
3
namespace Anax\Route;
4
5
/**
6
 * A container for routes.
7
 *
8
 */
9
class Route
10
{
11
    /**
12
     * @var string       $name      a name for this route.
13
     * @var string|array $method    the method(s) to support
14
     * @var string       $rule      the path rule for this route
15
     * @var callable     $action    the callback to handle this route
16
     * @var null|array   $arguments arguments for the callback, extracted
17
     *                              from path
18
     */
19
    private $name;
20
    private $method;
21
    private $rule;
22
    private $action;
23
    private $arguments = [];
24
25
26
27
    /**
28
     * Set values for route.
29
     *
30
     * @param null|string       $rule   for this route
31
     * @param callable          $action callable to implement a controller for
32
     *                                  the route
33
     * @param null|string|array $method as request method to support
34
     *
35
     * @return $this
36
     */
37 37
    public function set($rule, $action = null, $method = null)
38
    {
39 37
        $this->rule = $rule;
40 37
        $this->action = $action;
41
42 37
        $this->method = $method;
43 37
        if (is_string($method)) {
44 4
            $this->method = array_map("trim", explode("|", $method));
45 4
        }
46
47 37
        return $this;
48
    }
49
50
51
52
    /**
53
     * Check if part of route is a argument and optionally match type
54
     * as a requirement {argument:type}.
55
     *
56
     * @param string $rulePart   the rule part to check.
57
     * @param string $queryPart  the query part to check.
58
     * @param array  &$args      add argument to args array if matched
59
     *
60
     * @return boolean
61
     */
62 7
    private function checkPartAsArgument($rulePart, $queryPart, &$args)
63
    {
64 7
        if (substr($rulePart, -1) == "}"
65 7
            && !is_null($queryPart)
66 7
        ) {
67 7
            $part = substr($rulePart, 1, -1);
68 7
            $pos = strpos($part, ":");
69 7
            if ($pos !== false) {
70 2
                $type = substr($part, $pos + 1);
71 2
                if (! $this->checkPartMatchingType($queryPart, $type)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->checkPartMatchingType($queryPart, $type) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
Documentation introduced by
$type 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...
72 2
                    return false;
73
                }
74 2
            }
75 7
            $args[] = $this->typeConvertArgument($queryPart, $type);
0 ignored issues
show
Bug introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Documentation introduced by
$type 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...
76 7
            return true;
77
        }
78 3
        return false;
79
    }
80
81
82
83
    /**
84
     * Check if value is matching a certain type of values.
85
     *
86
     * @param string $value   the value to check.
87
     * @param array  $type    the expected type to check against.
88
     *
89
     * @return boolean
90
     */
91
    private function checkPartMatchingType($value, $type)
92 2
    {
93
        switch ($type) {
94
            case "digit":
95 2
                return ctype_digit($value);
96 2
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
97
98
            case "hex":
99 2
                return ctype_xdigit($value);
100 1
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
101
102
            case "alpha":
103 2
                return ctype_alpha($value);
104 2
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
105
106
            case "alphanum":
107 1
                return ctype_alnum($value);
108 1
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
109
110
            default:
111
                return false;
112
        }
113
    }
114
115
116
117
    /**
118
     * Check if value is matching a certain type and do type
119
     * conversion accordingly.
120
     *
121
     * @param string $value   the value to check.
122
     * @param array  $type    the expected type to check against.
123
     *
124
     * @return boolean
125
     */
126
    private function typeConvertArgument($value, $type)
127 30
    {
128
        switch ($type) {
129 30
            case "digit":
130 30
                return (int) $value;
131
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
132 30
133 1
            default:
134 1
                return $value;
135
        }
136 30
    }
137 7
138 7
139
140 29
    /**
141 29
     * Match part of rule and query.
142 29
     *
143 29
     * @param string $rulePart   the rule part to check.
144 30
     * @param string $queryPart  the query part to check.
145
     * @param array  &$args      add argument to args array if matched
146
     *
147
     * @return boolean
148
     */
149
    private function matchPart($rulePart, $queryPart, &$args)
150
    {
151
        $match = false;
0 ignored issues
show
Unused Code introduced by
$match is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
152
        $first = isset($rulePart[0]) ? $rulePart[0] : '';
153
        switch ($first) {
154
            case '*':
155
                $match = true;
156 36
                break;
157
158 36
            case '{':
159 36
                $match = $this->checkPartAsArgument($rulePart, $queryPart, $args);
160 36
                break;
161 8
162
            default:
163 36
                $match = ($rulePart == $queryPart);
164
                break;
165
        }
166
        return $match;
167
    }
168
169
170
171
    /**
172
     * Check if the request method matches.
173
     *
174
     * @param string $method as request method
175
     *
176 36
     * @return boolean true if request method matches
177
     */
178 36
    public function matchRequestMethod($method)
179 8
    {
180
        if ($method && is_array($this->method) && !in_array($method, $this->method)
181
            || (is_null($method) && !empty($this->method))
182
        ) {
183 36
            return false;
184 33
        }
185 36
        return true;
186 6
    }
187
188
189
190 30
    /**
191 30
     * Check if the route matches a query and request method.
192 30
     *
193 30
     * @param string $query  to match against
194
     * @param string $method as request method
195 30
     *
196 30
     * @return boolean true if query matches the route
197 30
     */
198
    public function match($query, $method = null)
199 30
    {
200 2
        if (!$this->matchRequestMethod($method)) {
201
            return false;
202
        }
203 30
204 14
        // If any/default */** route, match anything
205
        if (is_null($this->rule)
206 29
            || in_array($this->rule, ["*", "**"])
207
        ) {
208 29
            return true;
209 29
        }
210
211
        // Check all parts to see if they matches
212
        $ruleParts  = explode('/', $this->rule);
213
        $queryParts = explode('/', $query);
214
        $ruleCount = max(count($ruleParts), count($queryParts));
215
        $args = [];
216
217
        for ($i = 0; $i < $ruleCount; $i++) {
218
            $rulePart  = isset($ruleParts[$i])  ? $ruleParts[$i]  : null;
219 26
            $queryPart = isset($queryParts[$i]) ? $queryParts[$i] : null;
220
221 26
            if ($rulePart === "**") {
222
                break;
223
            }
224
225
            if (!$this->matchPart($rulePart, $queryPart, $args)) {
226
                return false;
227
            }
228
        }
229
230
        $this->arguments = $args;
231
        return true;
232
    }
233
234
235
236
    /**
237
     * Handle the action for the route.
238
     *
239
     * @param string $app container with services
240
     * 
241
     * @return void
242
     */
243
    public function handle($app = null)
244
    {
245
        if (is_callable($this->action)) {
246 24
            return call_user_func($this->action, ...$this->arguments);
247
        }
248 24
249
        // Try to load service from app injected container
250
        if ($app
0 ignored issues
show
Bug Best Practice introduced by
The expression $app of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
251
            && is_array($this->action)
252
            && isset($this->action[0])
253
            && isset($this->action[1])
254
            && is_string($this->action[0])
255
            && is_object($app->{$this->action[0]})
256
            && is_callable([$app->{$this->action[0]}, $this->action[1]])
257
        ) {
258
            return call_user_func(
259
                [$app->{$this->action[0]}, $this->action[1]],
260
                ...$this->arguments
261
            );
262
        }
263
    }
264
265
266
267
    /**
268
     * Set the name of the route.
269
     *
270
     * @param string $name set a name for the route
271
     *
272
     * @return $this
273
     */
274
    public function setName($name)
275
    {
276
        $this->name = $name;
277
        return $this;
278
    }
279
280
281
282
    /**
283
     * Get the rule for the route.
284
     *
285
     * @return string
286
     */
287
    public function getRule()
288
    {
289
        return $this->rule;
290
    }
291
292
293
294
    /**
295
     * Get the request method for the route.
296
     *
297
     * @return string representing the request method supported
298
     */
299
    public function getRequestMethod()
300
    {
301
        if (is_array($this->method)) {
302
            return implode("|", $this->method);
303
        }
304
305
        return $this->method;
306
    }
307
}
308