Completed
Push — master ( f97d4a...febe27 )
by Mikael
02:45
created

Route::matchRequestMethod()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 8.8571
cc 6
eloc 5
nc 2
nop 1
crap 6
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       $info      description of route.
14
     * @var string|array $method    the method(s) to support
15
     * @var string       $rule      the path rule for this route
16
     * @var callable     $action    the callback to handle this route
17
     * @var null|array   $arguments arguments for the callback, extracted
18
     *                              from path
19
     */
20
    private $name;
21
    private $info;
22
    private $method;
23
    private $rule;
24
    private $action;
25
    private $arguments = [];
26
27
28
29
    /**
30
     * Set values for route.
31
     *
32
     * @param null|string       $rule   for this route
33
     * @param callable          $action callable to implement a controller for
34
     *                                  the route
35
     * @param null|string|array $method as request method to support
36
     * @param null|string       $info   description of the route
37
     *
38
     * @return $this
39
     */
40 37
    public function set($rule, $action = null, $method = null, $info = null)
41
    {
42 37
        $this->rule = $rule;
43 37
        $this->action = $action;
44 37
        $this->info = $info;
45
46 37
        $this->method = $method;
47 37
        if (is_string($method)) {
48 4
            $this->method = array_map("trim", explode("|", $method));
49 4
        }
50 37
        if (is_array($this->method)) {
51 9
            $this->method = array_map("strtoupper", $this->method);
52 9
        }
53
54 37
        return $this;
55
    }
56
57
58
59
    /**
60
     * Check if part of route is a argument and optionally match type
61
     * as a requirement {argument:type}.
62
     *
63
     * @param string $rulePart   the rule part to check.
64
     * @param string $queryPart  the query part to check.
65
     * @param array  &$args      add argument to args array if matched
66
     *
67
     * @return boolean
68
     */
69 7
    private function checkPartAsArgument($rulePart, $queryPart, &$args)
70
    {
71 7
        if (substr($rulePart, -1) == "}"
72 7
            && !is_null($queryPart)
73 7
        ) {
74 7
            $part = substr($rulePart, 1, -1);
75 7
            $pos = strpos($part, ":");
76 7
            $type = null;
77 7
            if ($pos !== false) {
78 2
                $type = substr($part, $pos + 1);
79 2
                if (! $this->checkPartMatchingType($queryPart, $type)) {
0 ignored issues
show
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...
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...
80 2
                    return false;
81
                }
82 2
            }
83 7
            $args[] = $this->typeConvertArgument($queryPart, $type);
0 ignored issues
show
Documentation introduced by
$type is of type string|null, 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...
84 7
            return true;
85
        }
86 3
        return false;
87
    }
88
89
90
91
    /**
92
     * Check if value is matching a certain type of values.
93
     *
94
     * @param string $value   the value to check.
95
     * @param array  $type    the expected type to check against.
96
     *
97
     * @return boolean
98
     */
99 2
    private function checkPartMatchingType($value, $type)
100
    {
101
        switch ($type) {
102 2
            case "digit":
103 2
                return ctype_digit($value);
104
                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 2
            case "hex":
107 1
                return ctype_xdigit($value);
108
                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 2
            case "alpha":
111 2
                return ctype_alpha($value);
112
                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...
113
114 1
            case "alphanum":
115 1
                return ctype_alnum($value);
116
                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...
117
118
            default:
119
                return false;
120
        }
121
    }
122
123
124
125
    /**
126
     * Check if value is matching a certain type and do type
127
     * conversion accordingly.
128
     *
129
     * @param string $value   the value to check.
130
     * @param array  $type    the expected type to check against.
131
     *
132
     * @return boolean
133
     */
134 7
    private function typeConvertArgument($value, $type)
135
    {
136
        switch ($type) {
137 7
            case "digit":
138 2
                return (int) $value;
139
                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...
140
141 7
            default:
142 7
                return $value;
143 7
        }
144
    }
145
146
147
148
    /**
149
     * Match part of rule and query.
150
     *
151
     * @param string $rulePart   the rule part to check.
152
     * @param string $queryPart  the query part to check.
153
     * @param array  &$args      add argument to args array if matched
154
     *
155
     * @return boolean
156
     */
157 30
    private function matchPart($rulePart, $queryPart, &$args)
158
    {
159 30
        $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...
160 30
        $first = isset($rulePart[0]) ? $rulePart[0] : '';
161
        switch ($first) {
162 30
            case '*':
163 1
                $match = true;
164 1
                break;
165
166 30
            case '{':
167 7
                $match = $this->checkPartAsArgument($rulePart, $queryPart, $args);
168 7
                break;
169
170 29
            default:
171 29
                $match = ($rulePart == $queryPart);
172 29
                break;
173 29
        }
174 30
        return $match;
175
    }
176
177
178
179
    /**
180
     * Check if the request method matches.
181
     *
182
     * @param string $method as request method
183
     *
184
     * @return boolean true if request method matches
185
     */
186 36
    public function matchRequestMethod($method)
187
    {
188 36
        if ($method && is_array($this->method) && !in_array($method, $this->method)
189 36
            || (is_null($method) && !empty($this->method))
190 36
        ) {
191 8
            return false;
192
        }
193 36
        return true;
194
    }
195
196
197
198
    /**
199
     * Check if the route matches a query and request method.
200
     *
201
     * @param string $query  to match against
202
     * @param string $method as request method
203
     *
204
     * @return boolean true if query matches the route
205
     */
206 36
    public function match($query, $method = null)
207
    {
208 36
        if (!$this->matchRequestMethod($method)) {
209 8
            return false;
210
        }
211
212
        // If any/default */** route, match anything
213 36
        if (is_null($this->rule)
214 33
            || in_array($this->rule, ["*", "**"])
215 36
        ) {
216 6
            return true;
217
        }
218
219
        // Check all parts to see if they matches
220 30
        $ruleParts  = explode('/', $this->rule);
221 30
        $queryParts = explode('/', $query);
222 30
        $ruleCount = max(count($ruleParts), count($queryParts));
223 30
        $args = [];
224
225 30
        for ($i = 0; $i < $ruleCount; $i++) {
226 30
            $rulePart  = isset($ruleParts[$i])  ? $ruleParts[$i]  : null;
227 30
            $queryPart = isset($queryParts[$i]) ? $queryParts[$i] : null;
228
229 30
            if ($rulePart === "**") {
230 2
                break;
231
            }
232
233 30
            if (!$this->matchPart($rulePart, $queryPart, $args)) {
234 14
                return false;
235
            }
236 29
        }
237
238 29
        $this->arguments = $args;
239 29
        return true;
240
    }
241
242
243
244
    /**
245
     * Handle the action for the route.
246
     *
247
     * @param string $di container with services
248
     *
249
     * @return mixed
250
     */
251 26
    public function handle($di = null)
252
    {
253 26
        if (is_callable($this->action)) {
254 26
            return call_user_func($this->action, ...$this->arguments);
255
        }
256
257
        // Try to load service from app/di injected container
258
        if ($di
0 ignored issues
show
Bug Best Practice introduced by
The expression $di 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...
259
            && is_array($this->action)
260
            && isset($this->action[0])
261
            && isset($this->action[1])
262
            && is_string($this->action[0])
263
            && $di->has($this->action[0])
0 ignored issues
show
Bug introduced by
The method has cannot be called on $di (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
264
        ) {
265
            $service = $di->get($this->action[0]);
0 ignored issues
show
Bug introduced by
The method get cannot be called on $di (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
266
            if (is_callable([$service, $this->action[1]])) {
267
                return call_user_func(
268
                    [$service, $this->action[1]],
269
                    ...$this->arguments
270
                );
271
            }
272
        }
273
    }
274
275
276
277
    /**
278
     * Set the name of the route.
279
     *
280
     * @param string $name set a name for the route
281
     *
282
     * @return $this
283
     */
284
    public function setName($name)
285
    {
286
        $this->name = $name;
287
        return $this;
288
    }
289
290
291
292
    /**
293
     * Get information of the route.
294
     *
295
     * @return null|string as route information.
296
     */
297
    public function getInfo()
298
    {
299
        return $this->info;
300
    }
301
302
303
304
    /**
305
     * Get the rule for the route.
306
     *
307
     * @return string
308
     */
309 24
    public function getRule()
310
    {
311 24
        return $this->rule;
312
    }
313
314
315
316
    /**
317
     * Get the request method for the route.
318
     *
319
     * @return string representing the request method supported
320
     */
321
    public function getRequestMethod()
322
    {
323
        if (is_array($this->method)) {
324
            return implode("|", $this->method);
325
        }
326
327
        return $this->method;
328
    }
329
}
330