Completed
Push — master ( 7a41c6...837337 )
by Mikael
05:54
created

Route   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 69.16%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 1
dl 0
loc 332
ccs 74
cts 107
cp 0.6916
rs 8.48
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 16 3
A checkPartAsArgument() 0 19 5
A matchRequestMethod() 0 9 6
B match() 0 35 9
B handle() 0 34 10
A setName() 0 5 1
A getInfo() 0 4 1
A getRule() 0 4 1
A getRequestMethod() 0 8 2
A checkPartMatchingType() 0 23 5
A typeConvertArgument() 0 11 2
A matchPart() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Route often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Route, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Anax\Route;
4
5
use \Anax\Route\Exception\ConfigurationException;
6
7
/**
8
 * A container for routes.
9
 *
10
 */
11
class Route
12
{
13
    /**
14
     * @var string       $name      a name for this route.
15
     * @var string       $info      description of route.
16
     * @var string|array $method    the method(s) to support
17
     * @var string       $rule      the path rule for this route
18
     * @var callable     $action    the callback to handle this route
19
     * @var null|array   $arguments arguments for the callback, extracted
20
     *                              from path
21
     */
22
    private $name;
23
    private $info;
24
    private $method;
25
    private $rule;
26
    private $action;
27
    private $arguments = [];
28
29
30
31
    /**
32
     * Set values for route.
33
     *
34
     * @param null|string       $rule   for this route
35
     * @param callable          $action callable to implement a controller for
36
     *                                  the route
37
     * @param null|string|array $method as request method to support
38
     * @param null|string       $info   description of the route
39
     *
40
     * @return $this
41
     */
42 43
    public function set($rule, $action = null, $method = null, $info = null)
43
    {
44 43
        $this->rule = $rule;
45 43
        $this->action = $action;
46 43
        $this->info = $info;
47
48 43
        $this->method = $method;
49 43
        if (is_string($method)) {
50 4
            $this->method = array_map("trim", explode("|", $method));
51
        }
52 43
        if (is_array($this->method)) {
53 9
            $this->method = array_map("strtoupper", $this->method);
54
        }
55
56 43
        return $this;
57
    }
58
59
60
61
    /**
62
     * Check if part of route is a argument and optionally match type
63
     * as a requirement {argument:type}.
64
     *
65
     * @param string $rulePart   the rule part to check.
66
     * @param string $queryPart  the query part to check.
67
     * @param array  &$args      add argument to args array if matched
68
     *
69
     * @return boolean
70
     */
71 6
    private function checkPartAsArgument($rulePart, $queryPart, &$args)
72
    {
73 6
        if (substr($rulePart, -1) == "}"
74 6
            && !is_null($queryPart)
75
        ) {
76 6
            $part = substr($rulePart, 1, -1);
77 6
            $pos = strpos($part, ":");
78 6
            $type = null;
79 6
            if ($pos !== false) {
80 2
                $type = substr($part, $pos + 1);
81 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...
82 2
                    return false;
83
                }
84
            }
85 6
            $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...
86 6
            return true;
87
        }
88 2
        return false;
89
    }
90
91
92
93
    /**
94
     * Check if value is matching a certain type of values.
95
     *
96
     * @param string $value   the value to check.
97
     * @param array  $type    the expected type to check against.
98
     *
99
     * @return boolean
100
     */
101 2
    private function checkPartMatchingType($value, $type)
102
    {
103 2
        switch ($type) {
104
            case "digit":
105 2
                return ctype_digit($value);
106
                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...
107
108
            case "hex":
109 1
                return ctype_xdigit($value);
110
                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...
111
112
            case "alpha":
113 2
                return ctype_alpha($value);
114
                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...
115
116
            case "alphanum":
117 1
                return ctype_alnum($value);
118
                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...
119
120
            default:
121
                return false;
122
        }
123
    }
124
125
126
127
    /**
128
     * Check if value is matching a certain type and do type
129
     * conversion accordingly.
130
     *
131
     * @param string $value   the value to check.
132
     * @param array  $type    the expected type to check against.
133
     *
134
     * @return boolean
135
     */
136 6
    private function typeConvertArgument($value, $type)
137
    {
138 6
        switch ($type) {
139 4
            case "digit":
140 2
                return (int) $value;
141
                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...
142
143
            default:
144 6
                return $value;
145
        }
146
    }
147
148
149
150
    /**
151
     * Match part of rule and query.
152
     *
153
     * @param string $rulePart   the rule part to check.
154
     * @param string $queryPart  the query part to check.
155
     * @param array  &$args      add argument to args array if matched
156
     *
157
     * @return boolean
158
     */
159 35
    private function matchPart($rulePart, $queryPart, &$args)
160
    {
161 35
        $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...
162 35
        $first = isset($rulePart[0]) ? $rulePart[0] : '';
163 35
        switch ($first) {
164
            case '*':
165 3
                $match = true;
166 3
                break;
167
168
            case '{':
169 6
                $match = $this->checkPartAsArgument($rulePart, $queryPart, $args);
170 6
                break;
171
172
            default:
173 34
                $match = ($rulePart == $queryPart);
174 34
                break;
175
        }
176 35
        return $match;
177
    }
178
179
180
181
    /**
182
     * Check if the request method matches.
183
     *
184
     * @param string $method as request method
185
     *
186
     * @return boolean true if request method matches
187
     */
188 42
    public function matchRequestMethod($method)
189
    {
190 42
        if ($method && is_array($this->method) && !in_array($method, $this->method)
191 42
            || (is_null($method) && !empty($this->method))
192
        ) {
193 8
            return false;
194
        }
195 42
        return true;
196
    }
197
198
199
200
    /**
201
     * Check if the route matches a query and request method.
202
     *
203
     * @param string $query  to match against
204
     * @param string $method as request method
205
     *
206
     * @return boolean true if query matches the route
207
     */
208 42
    public function match($query, $method = null)
209
    {
210 42
        if (!$this->matchRequestMethod($method)) {
211 8
            return false;
212
        }
213
214
        // If any/default */** route, match anything
215 42
        if (is_null($this->rule)
216 42
            || in_array($this->rule, ["*", "**"])
217
        ) {
218 7
            return true;
219
        }
220
221
        // Check all parts to see if they matches
222 35
        $ruleParts  = explode('/', $this->rule);
223 35
        $queryParts = explode('/', $query);
224 35
        $ruleCount = max(count($ruleParts), count($queryParts));
225 35
        $args = [];
226
227 35
        for ($i = 0; $i < $ruleCount; $i++) {
228 35
            $rulePart  = isset($ruleParts[$i])  ? $ruleParts[$i]  : null;
229 35
            $queryPart = isset($queryParts[$i]) ? $queryParts[$i] : null;
230
231 35
            if ($rulePart === "**") {
232 3
                break;
233
            }
234
235 35
            if (!$this->matchPart($rulePart, $queryPart, $args)) {
236 17
                return false;
237
            }
238
        }
239
240 34
        $this->arguments = $args;
241 34
        return true;
242
    }
243
244
245
246
    /**
247
     * Handle the action for the route.
248
     *
249
     * @param string $di container with services
250
     *
251
     * @return mixed
252
     */
253 32
    public function handle($di = null)
254
    {
255 32
        if (is_callable($this->action)) {
256 32
            return call_user_func($this->action, ...$this->arguments);
257
        }
258
259
        // Try to load service from app/di injected container
260
        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...
261
            && is_array($this->action)
262
            && isset($this->action[0])
263
            && isset($this->action[1])
264
            && is_string($this->action[0])
265
        ) {
266
            if (!$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...
267
                throw new ConfigurationException("Routehandler '{$this->action[0]}' not loaded in di.");
268
            }
269
270
            $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...
271
            if (!is_callable([$service, $this->action[1]])) {
272
                throw new ConfigurationException(
273
                    "Routehandler '{$this->action[0]}' does not have a callable method '{$this->action[1]}'."
274
                );
275
            }
276
277
            return call_user_func(
278
                [$service, $this->action[1]],
279
                ...$this->arguments
280
            );
281
        }
282
283
        if (!is_null($this->action)) {
284
            throw new ConfigurationException("Routehandler '{$this->rule}' does not have a callable action.");
285
        }
286
    }
287
288
289
290
    /**
291
     * Set the name of the route.
292
     *
293
     * @param string $name set a name for the route
294
     *
295
     * @return $this
296
     */
297
    public function setName($name)
298
    {
299
        $this->name = $name;
300
        return $this;
301
    }
302
303
304
305
    /**
306
     * Get information of the route.
307
     *
308
     * @return null|string as route information.
309
     */
310
    public function getInfo()
311
    {
312
        return $this->info;
313
    }
314
315
316
317
    /**
318
     * Get the rule for the route.
319
     *
320
     * @return string
321
     */
322 30
    public function getRule()
323
    {
324 30
        return $this->rule;
325
    }
326
327
328
329
    /**
330
     * Get the request method for the route.
331
     *
332
     * @return string representing the request method supported
333
     */
334
    public function getRequestMethod()
335
    {
336
        if (is_array($this->method)) {
337
            return implode("|", $this->method);
338
        }
339
340
        return $this->method;
341
    }
342
}
343