Passed
Branch master (11b479)
by Mikael
03:34
created

src/Route/Route.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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