Issues (3)

src/Router.php (1 issue)

1
<?php
2
3
/**
4
 * Platine Router
5
 *
6
 * Platine Router is the a lightweight and simple router using middleware
7
 *  to match and dispatch the request.
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Router
12
 * Copyright (c) 2020 Evgeniy Zyubin
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Router.php
35
 *
36
 *  The Router class is used to route the request to the handler
37
 *  for response generation
38
 *
39
 *  @package    Platine\Route
40
 *  @author Platine Developers Team
41
 *  @copyright  Copyright (c) 2020
42
 *  @license    http://opensource.org/licenses/MIT  MIT License
43
 *  @link   https://www.platine-php.com
44
 *  @version 1.0.0
45
 *  @filesource
46
 */
47
48
declare(strict_types=1);
49
50
namespace Platine\Route;
51
52
use Platine\Http\ServerRequestInterface;
53
use Platine\Http\UriInterface;
54
use Platine\Route\Exception\RouteNotFoundException;
55
56
/**
57
 * @class Router
58
 * @package Platine\Route
59
 */
60
class Router
61
{
62
    /**
63
     * The current route group prefix
64
     * @var string
65
     */
66
    protected string $groupPrefix = '';
67
68
    /**
69
     * The instance of RouteCollectionInterface
70
     * @var  RouteCollectionInterface
71
     */
72
    protected RouteCollectionInterface $routes;
73
74
    /**
75
     * The base path to use
76
     * @var string
77
     */
78
    protected string $basePath = '/';
79
80
    /**
81
     * Create new Router instance
82
     * @param RouteCollectionInterface|null $routes
83
     */
84
    public function __construct(?RouteCollectionInterface $routes = null)
85
    {
86
        $this->routes = $routes ?? new RouteCollection();
87
    }
88
89
    /**
90
     * Set base path
91
     * @param string $basePath
92
     * @return $this
93
     */
94
    public function setBasePath(string $basePath): self
95
    {
96
        $this->basePath = $basePath;
97
98
        return $this;
99
    }
100
101
102
    /**
103
     * Return the instance of route collection with all routes set.
104
     *
105
     * @return RouteCollectionInterface
106
     */
107
    public function routes(): RouteCollectionInterface
108
    {
109
        return $this->routes;
110
    }
111
112
    /**
113
     * Create a route group with a common prefix.
114
     *
115
     * The callback can take a Router instance as a parameter.
116
     * All routes created in the passed callback will have the given group prefix prepended.
117
     *
118
     * @param  string   $prefix   common path prefix for the route group.
119
     * @param  callable $callback callback that will add routes with a common path prefix.
120
     * @return void
121
     */
122
    public function group(string $prefix, callable $callback): void
123
    {
124
        $currentGroupPrefix = $this->groupPrefix;
125
        $this->groupPrefix = $currentGroupPrefix . $prefix;
126
        $callback($this);
127
        $this->groupPrefix = $currentGroupPrefix;
128
    }
129
130
    /**
131
     * Add new route and return it
132
     * @param string $pattern path pattern with parameters.
133
     * @param mixed $handler action, controller, callable, closure, etc.
134
     * @param string[]|string  $methods allowed request methods of the route.
135
     * @param string $name  the  route name.
136
     * @param array<string, mixed> $attributes the route attributes.
137
     *
138
     * @return Route
139
     */
140
    public function add(
141
        string $pattern,
142
        mixed $handler,
143
        array|string $methods,
144
        string $name = '',
145
        array $attributes = []
146
    ): Route {
147
        $pattern = $this->groupPrefix . $pattern;
148
        $route = new Route(
149
            $pattern,
150
            $handler,
151
            $name,
152
            $methods,
153
            $attributes
154
        );
155
        $this->routes->add($route);
156
157
        return $route;
158
    }
159
160
161
    /**
162
     * Add a generic route for any request methods and returns it.
163
     *
164
     * @param  string $pattern path pattern with parameters.
165
     * @param  mixed $handler action, controller, callable, closure, etc.
166
     * @param  string $name the  route name.
167
     * @param array<string, mixed> $attributes the route attributes.
168
     * @return Route the new route added
169
     */
170
    public function any(
171
        string $pattern,
172
        mixed $handler,
173
        string $name = '',
174
        array $attributes = []
175
    ): Route {
176
        return $this->add(
177
            $pattern,
178
            $handler,
179
            [],
180
            $name,
181
            $attributes
182
        );
183
    }
184
185
    /**
186
     * Add a generic route for form request methods and returns it.
187
     *
188
     * @param  string $pattern path pattern with parameters.
189
     * @param  mixed $handler action, controller, callable, closure, etc.
190
     * @param  string $name the  route name.
191
     * @param array<string, mixed> $attributes the route attributes.
192
     * @return Route the new route added
193
     */
194
    public function form(
195
        string $pattern,
196
        mixed $handler,
197
        string $name = '',
198
        array $attributes = []
199
    ): Route {
200
        return $this->add(
201
            $pattern,
202
            $handler,
203
            ['GET', 'POST'],
204
            $name,
205
            $attributes
206
        );
207
    }
208
209
    /**
210
     * Add a GET route and returns it.
211
     *
212
     * @see  Router::add
213
     * @param mixed $handler action, controller, callable, closure, etc.
214
     * @param array<string, mixed> $attributes the route attributes.
215
     */
216
    public function get(
217
        string $pattern,
218
        mixed $handler,
219
        string $name = '',
220
        array $attributes = []
221
    ): Route {
222
        return $this->add(
223
            $pattern,
224
            $handler,
225
            ['GET'],
226
            $name,
227
            $attributes
228
        );
229
    }
230
231
    /**
232
     * Add a POST route and returns it.
233
     *
234
     * @see  Router::add
235
     * @param mixed $handler action, controller, callable, closure, etc.
236
     * @param array<string, mixed> $attributes the route attributes.
237
     */
238
    public function post(
239
        string $pattern,
240
        mixed $handler,
241
        string $name = '',
242
        array $attributes = []
243
    ): Route {
244
        return $this->add(
245
            $pattern,
246
            $handler,
247
            ['POST'],
248
            $name,
249
            $attributes
250
        );
251
    }
252
253
    /**
254
     * Add a PUT route and returns it.
255
     *
256
     * @see  Router::add
257
     * @param mixed $handler action, controller, callable, closure, etc.
258
     * @param array<string, mixed> $attributes the route attributes.
259
     */
260
    public function put(
261
        string $pattern,
262
        mixed $handler,
263
        string $name = '',
264
        array $attributes = []
265
    ): Route {
266
        return $this->add(
267
            $pattern,
268
            $handler,
269
            ['PUT'],
270
            $name,
271
            $attributes
272
        );
273
    }
274
275
    /**
276
     * Add a PATCH route and returns it.
277
     *
278
     * @see  Router::add
279
     * @param mixed $handler action, controller, callable, closure, etc.
280
     * @param array<string, mixed> $attributes the route attributes.
281
     */
282
    public function patch(
283
        string $pattern,
284
        mixed $handler,
285
        string $name = '',
286
        array $attributes = []
287
    ): Route {
288
        return $this->add(
289
            $pattern,
290
            $handler,
291
            ['PATCH'],
292
            $name,
293
            $attributes
294
        );
295
    }
296
297
    /**
298
     * Add a DELETE route and returns it.
299
     *
300
     * @see  Router::add
301
     * @param mixed $handler action, controller, callable, closure, etc.
302
     * @param array<string, mixed> $attributes the route attributes.
303
     */
304
    public function delete(
305
        string $pattern,
306
        mixed $handler,
307
        string $name = '',
308
        array $attributes = []
309
    ): Route {
310
        return $this->add(
311
            $pattern,
312
            $handler,
313
            ['DELETE'],
314
            $name,
315
            $attributes
316
        );
317
    }
318
319
    /**
320
     * Add a HEAD route and returns it.
321
     *
322
     * @see  Router::add
323
     * @param mixed $handler action, controller, callable, closure, etc.
324
     * @param array<string, mixed> $attributes the route attributes.
325
     */
326
    public function head(
327
        string $pattern,
328
        mixed $handler,
329
        string $name = '',
330
        array $attributes = []
331
    ): Route {
332
        return $this->add(
333
            $pattern,
334
            $handler,
335
            ['HEAD'],
336
            $name,
337
            $attributes
338
        );
339
    }
340
341
    /**
342
     * Add a OPTIONS route and returns it.
343
     *
344
     * @see  Router::add
345
     * @param mixed $handler action, controller, callable, closure, etc.
346
     * @param array<string, mixed> $attributes the route attributes.
347
     */
348
    public function options(
349
        string $pattern,
350
        mixed $handler,
351
        string $name = '',
352
        array $attributes = []
353
    ): Route {
354
        return $this->add(
355
            $pattern,
356
            $handler,
357
            ['OPTIONS'],
358
            $name,
359
            $attributes
360
        );
361
    }
362
363
    /**
364
     * Add a resource route.
365
     *
366
     * @param  string $pattern path pattern with parameters.
367
     * @param  class-string $handler action class.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
368
     * @param  string $name the  route name and permission prefix.
369
     * @param  bool $permission whether to use permission.
370
     * @param array<string, mixed> $attributes the route attributes.
371
     * @return $this
372
     */
373
    public function resource(
374
        string $pattern,
375
        string $handler,
376
        string $name = '',
377
        bool $permission = true,
378
        array $attributes = []
379
    ): self {
380
381
        $maps = [
382
            [
383
                'path' => '',
384
                'action' => '%s@index',
385
                'route_name' => '%s_list',
386
                'method' => 'get',
387
                'csrf' => false,
388
                'many' => false,
389
                'permission' => $permission ? '%s_list' : null,
390
            ],
391
            [
392
                'path' => '/detail/{id}',
393
                'action' => '%s@detail',
394
                'route_name' => '%s_detail',
395
                'method' => 'get',
396
                'csrf' => false,
397
                'many' => false,
398
                'permission' => $permission ? '%s_detail' : null,
399
            ],
400
            [
401
                'path' => '/create',
402
                'action' => '%s@create',
403
                'route_name' => '%s_create',
404
                'method' => 'add',
405
                'csrf' => false,
406
                'many' => true,
407
                'permission' => $permission ? '%s_create' : null,
408
            ],
409
            [
410
                'path' => '/update/{id}',
411
                'action' => '%s@update',
412
                'route_name' => '%s_update',
413
                'method' => 'add',
414
                'csrf' => false,
415
                'many' => true,
416
                'permission' => $permission ? '%s_update' : null,
417
            ],
418
            [
419
                'path' => '/delete/{id}',
420
                'action' => '%s@delete',
421
                'route_name' => '%s_delete',
422
                'method' => 'get',
423
                'csrf' => true,
424
                'many' => false,
425
                'permission' => $permission ? '%s_delete' : null,
426
            ],
427
        ];
428
429
        if (empty($name)) {
430
            $name = trim($pattern, '/');
431
        }
432
433
        $this->group($pattern, function (Router $router) use ($handler, $maps, $name, $attributes) {
434
            foreach ($maps as $map) {
435
                if ($map['many']) {
436
                    /** @var Route $route */
437
                    $route = $router->{$map['method']}(
438
                        $map['path'],
439
                        sprintf($map['action'], $handler),
440
                        ['GET', 'POST'],
441
                        sprintf($map['route_name'], $name),
442
                        $attributes
443
                    );
444
                } else {
445
                    /** @var Route $route */
446
                    $route = $router->{$map['method']}(
447
                        $map['path'],
448
                        sprintf($map['action'], $handler),
449
                        sprintf($map['route_name'], $name),
450
                        $attributes
451
                    );
452
                }
453
454
                if ($map['permission']) {
455
                    $route->setAttribute('permission', sprintf($map['permission'], $name));
456
                }
457
458
                if ($map['csrf']) {
459
                    $route->setAttribute('csrf', true);
460
                }
461
            }
462
        });
463
464
        return $this;
465
    }
466
467
468
    /**
469
     * Matches the request against known routes.
470
     * @param  ServerRequestInterface $request
471
     * @param  bool $checkAllowedMethods whether to check if the
472
     * request method matches the allowed route methods.
473
     * @return Route|null matched route or null if the
474
     * request does not match any route.
475
     */
476
    public function match(
477
        ServerRequestInterface $request,
478
        bool $checkAllowedMethods = true
479
    ): ?Route {
480
        $notAllowedMethodRoute = null;
481
        foreach ($this->routes->all() as $route) {
482
            if ($route->match($request, $this->basePath) === false) {
483
                continue;
484
            }
485
486
            if ($route->isAllowedMethod($request->getMethod())) {
487
                return $route;
488
            }
489
490
            if ($notAllowedMethodRoute === null) {
491
                $notAllowedMethodRoute = $route;
492
            }
493
        }
494
495
        return $checkAllowedMethods ? null : $notAllowedMethodRoute;
496
    }
497
498
    /**
499
     * Return the Uri for this route
500
     * @param  string  $name the route name
501
     * @param  array<string, mixed>  $parameters the route parameters
502
     * @return UriInterface
503
     *
504
     * @throws RouteNotFoundException if the route does not exist.
505
     */
506
    public function getUri(string $name, array $parameters = []): UriInterface
507
    {
508
        if ($this->routes->has($name)) {
509
            return $this->routes->get($name)
510
                                ->getUri($parameters, $this->basePath);
511
        }
512
513
        throw new RouteNotFoundException(sprintf('Route [%s] not found', $name));
514
    }
515
516
    /**
517
     * Generates the URL path from the named route and parameters.
518
     * @param  string $name
519
     * @param  array<string, mixed>  $parameters
520
     * @return string
521
     *
522
     * @throws RouteNotFoundException if the route does not exist.
523
     */
524
    public function path(string $name, array $parameters = []): string
525
    {
526
        return $this->getUri($name, $parameters)->getPath();
527
    }
528
}
529