Completed
Push — master ( c8fcb0...10d838 )
by Alex
01:42
created

UrlParser.php (5 issues)

1
<?php
2
namespace Mezon\Router;
3
4
trait UrlParser
5
{
6
7
    /**
8
     * Supported types of URL parameters
9
     *
10
     * @var array
11
     */
12
    private $types = [];
13
14
    /**
15
     * Parsed parameters of the calling router
16
     *
17
     * @var array
18
     */
19
    protected $parameters = [];
20
21
    /**
22
     * Method handles integer type
23
     *
24
     * @param string $value
25
     *            value to be parsed
26
     * @return bool was the value parsed
27
     */
28
    public static function intHandler(string &$value): bool
29
    {
30
        if (is_numeric($value)) {
31
            $value = $value + 0;
32
            return true;
33
        }
34
35
        return false;
36
    }
37
38
    /**
39
     * Method handles command type
40
     *
41
     * @param string $value
42
     *            value to be parsed
43
     * @return bool was the value parsed
44
     */
45
    public static function commandHandler(string &$value): bool
46
    {
47
        if (preg_match('/^([a-z0-9A-Z_\/\-\.\@]+)$/', $value)) {
48
            return true;
49
        }
50
51
        return false;
52
    }
53
54
    /**
55
     * Method handles list of integers type
56
     *
57
     * @param string $value
58
     *            value to be parsed
59
     * @return bool was the value parsed
60
     */
61
    public static function intListHandler(string &$value): bool
62
    {
63
        if (preg_match('/^([0-9,]+)$/', $value)) {
64
            return true;
65
        }
66
67
        return false;
68
    }
69
70
    /**
71
     * Method handles string type
72
     *
73
     * @param string $value
74
     *            value to be parsed
75
     * @return bool was the value parsed
76
     */
77
    public static function stringHandler(string &$value): bool
78
    {
79
        $value = htmlspecialchars($value, ENT_QUOTES);
80
81
        return true;
82
    }
83
84
    /**
85
     * Init types
86
     */
87
    protected function initDefaultTypes()
88
    {
89
        $this->types['i'] = '\Mezon\Router\UrlParser::intHandler';
90
        $this->types['a'] = '\Mezon\Router\UrlParser::commandHandler';
91
        $this->types['il'] = '\Mezon\Router\UrlParser::intListHandler';
92
        $this->types['s'] = '\Mezon\Router\UrlParser::stringHandler';
93
    }
94
95
    /**
96
     * Matching parameter and component
97
     *
98
     * @param mixed $component
99
     *            Component of the URL
100
     * @param string $parameter
101
     *            Parameter to be matched
102
     * @return string Matched url parameter
103
     */
104
    private function _matchParameterAndComponent(&$component, string $parameter)
105
    {
106
        $parameterData = explode(':', trim($parameter, '[]'));
107
108
        if (isset($this->types[$parameterData[0]])) {
109
            if ($this->types[$parameterData[0]]($component)) {
110
                return $parameterData[1];
111
            } else {
112
                return '';
113
            }
114
        } else {
115
            throw (new \Exception('Unknown parameter type : ' . $parameterData[0]));
116
        }
117
    }
118
119
    /**
120
     * Method matches route and pattern
121
     *
122
     * @param array $cleanRoute
123
     *            Cleaned route splitted in parts
124
     * @param array $cleanPattern
125
     *            Route pattern
126
     * @return array|bool Array of route's parameters
127
     */
128
    private function _matchRouteAndPattern(array $cleanRoute, array $cleanPattern)
129
    {
130
        if (count($cleanRoute) !== count($cleanPattern)) {
131
            return false;
132
        }
133
134
        $paremeters = [];
135
        $patternsCount = count($cleanPattern);
136
137
        for ($i = 0; $i < $patternsCount; $i ++) {
138
            if (\Mezon\Router\Utils::isParameter($cleanPattern[$i])) {
139
                $parameterName = $this->_matchParameterAndComponent($cleanRoute[$i], $cleanPattern[$i]);
140
141
                // it's a parameter
142
                if ($parameterName !== '') {
143
                    // parameter was matched, store it!
144
                    $paremeters[$parameterName] = $cleanRoute[$i];
145
                } else {
146
                    return false;
147
                }
148
            } else {
149
                // it's a static part of the route
150
                if ($cleanRoute[$i] !== $cleanPattern[$i]) {
151
                    return false;
152
                }
153
            }
154
        }
155
156
        $this->parameters = $paremeters;
157
    }
158
159
    /**
160
     * Method searches dynamic route processor
161
     *
162
     * @param array $processors
163
     *            Callable router's processor
164
     * @param string $route
165
     *            Route
166
     * @return array|callable route's handler
167
     */
168
    protected function getDynamicRouteProcessor(array &$processors, string $route)
169
    {
170
        $cleanRoute = explode('/', trim($route, '/'));
171
172
        foreach ($processors as $i => $processor) {
173
            $cleanPattern = explode('/', trim($i, '/'));
174
175
            if ($this->_matchRouteAndPattern($cleanRoute, $cleanPattern) !== false) {
176
                return $processor;
177
            }
178
        }
179
180
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array|callable.
Loading history...
181
    }
182
183
    /**
184
     * Method searches dynamic route processor
185
     *
186
     * @param array $processors
187
     *            Callable router's processor
188
     * @param string $route
189
     *            Route
190
     * @return string|bool Result of the router'scall or false if any error occured
191
     */
192
    public function findDynamicRouteProcessor(array &$processors, string $route)
193
    {
194
        $cleanRoute = explode('/', trim($route, '/'));
195
196
        foreach ($processors as $i => $processor) {
197
            $cleanPattern = explode('/', trim($i, '/'));
198
199
            if ($this->_matchRouteAndPattern($cleanRoute, $cleanPattern) !== false) {
200
                return call_user_func($processor, $route, $this->parameters); // return result of the router
201
            }
202
        }
203
204
        return false;
205
    }
206
207
    /**
208
     * Checking that method exists
209
     *
210
     * @param object|array $processor
211
     *            callback object
212
     * @param ?string $functionName
213
     *            callback method
214
     * @return bool true if method does not exists
215
     */
216
    private function methodDoesNotExists($processor, ?string $functionName): bool
217
    {
218
        return isset($processor[0]) && method_exists($processor[0], $functionName) === false;
219
    }
220
221
    /**
222
     * Checking that handler can be called
223
     *
224
     * @param object|array $processor
225
     *            callback object
226
     * @param ?string $functionName
227
     *            callback method
228
     * @return bool
229
     */
230
    private function canBeCalled($processor, ?string $functionName): bool
231
    {
232
        return is_callable($processor) &&
233
            (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName));
234
    }
235
236
    /**
237
     * Checking that processor can be called as function
238
     *
239
     * @param mixed $processor
240
     *            route processor
241
     * @return bool true if the $processor can be called as function
242
     */
243
    private function isFunction($processor): bool
244
    {
245
        return is_callable($processor) && is_array($processor) === false;
246
    }
247
248
    /**
249
     * Method returns either universal hanler if it fits or normal handler
250
     *
251
     * @param array $processors
252
     *            list of routes and handlers
253
     * @param string $route
254
     *            calling route
255
     * @return mixed processor
256
     */
257
    protected function getExactRouteHandlerOrUniversal(&$processors, string $route)
258
    {
259
        if ($this->universalRouteWasAdded) {
260
            $allRoutes = array_keys($processors);
261
262
            if (array_search('*', $allRoutes) <= array_search($route, $allRoutes)) {
263
                $processor = $processors['*'];
264
            } else {
265
                $processor = $processors[$route];
266
            }
267
        } else {
268
            $processor = $processors[$route];
269
        }
270
271
        return $processor;
272
    }
273
274
    /**
275
     * Method executes route handler
276
     *
277
     * @param array|callable $processor
278
     * @param string $route
279
     * @return mixed route handler execution result
280
     */
281
    protected function executeHandler($processor, string $route)
282
    {
283
        if ($this->isFunction($processor)) {
284
            return $processor($route, []);
285
        }
286
287
        $functionName = $processor[1] ?? null;
288
289
        if ($this->canBeCalled($processor, $functionName)) {
0 ignored issues
show
It seems like $processor can also be of type callable; however, parameter $processor of Mezon\Router\UrlParser::canBeCalled() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

289
        if ($this->canBeCalled(/** @scrutinizer ignore-type */ $processor, $functionName)) {
Loading history...
290
            // passing route path and parameters
291
            return call_user_func($processor, $route, []);
292
        } else {
293
            $callableDescription = \Mezon\Router\Utils::getCallableDescription($processor);
294
295
            if ($this->methodDoesNotExists($processor, $functionName)) {
0 ignored issues
show
It seems like $processor can also be of type callable; however, parameter $processor of Mezon\Router\UrlParser::methodDoesNotExists() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
            if ($this->methodDoesNotExists(/** @scrutinizer ignore-type */ $processor, $functionName)) {
Loading history...
296
                throw (new \Exception("'$callableDescription' does not exists"));
297
            } else {
298
                throw (new \Exception("'$callableDescription' must be callable entity"));
299
            }
300
        }
301
    }
302
303
    /**
304
     * Method returns route handler
305
     *
306
     * @param mixed $processors
307
     *            Callable router's processor
308
     * @param string $route
309
     *            Route
310
     * @return array|callable route handler
311
     */
312
    protected function getStaticRouteProcessor(&$processors, string $route)
313
    {
314
        if (isset($processors[$route])) {
315
            $processor = $this->getExactRouteHandlerOrUniversal($processors, $route);
316
        } elseif (isset($processors['*'])) {
317
            $processor = $processors['*'];
318
        } else {
319
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array|callable.
Loading history...
320
        }
321
322
        return $processor;
323
    }
324
325
    /**
326
     * Method searches route processor
327
     *
328
     * @param mixed $processors
329
     *            Callable router's processor
330
     * @param string $route
331
     *            Route
332
     * @return mixed Result of the router processor
333
     */
334
    public function findStaticRouteProcessor(&$processors, string $route)
335
    {
336
        $processor = $this->getStaticRouteProcessor($processors, $route);
337
338
        if ($processor === false) {
0 ignored issues
show
The condition $processor === false is always false.
Loading history...
339
            return false;
340
        }
341
342
        return $this->executeHandler($processor, $route);
343
    }
344
345
    /**
346
     * Method returns route parameter
347
     *
348
     * @param string $name
349
     *            Route parameter
350
     * @return string Route parameter
351
     */
352
    public function getParam(string $name): string
353
    {
354
        if (isset($this->parameters[$name]) === false) {
355
            throw (new \Exception('Parameter ' . $name . ' was not found in route', - 1));
356
        }
357
358
        return $this->parameters[$name];
359
    }
360
361
    /**
362
     * Does parameter exists
363
     *
364
     * @param string $name
365
     *            Param name
366
     * @return bool True if the parameter exists
367
     */
368
    public function hasParam(string $name): bool
369
    {
370
        return isset($this->parameters[$name]);
371
    }
372
}