├── crud-definition.png
├── resource-http-verbs.png
├── .gitignore
├── phpstan.neon
├── tests
├── routes-2.php
├── routes-1.php
├── TheRouteFactory.php
├── DelimiterTest.php
├── CacheTest.php
├── CrudTest.php
├── route-cache.php
├── ParameterTypeTest.php
├── DispatcherTest.php
├── CollectorTest.php
└── RouteRegisterTest.php
├── phpunit.xml
├── composer.json
├── LICENSE
├── src
├── Router
│ ├── DispatchResult.php
│ ├── Cache.php
│ ├── RouteData.php
│ ├── Dispatcher.php
│ ├── Collector.php
│ ├── Getter.php
│ └── TheRoute.php
├── Route.php
├── Crud.php
└── RouteInterface.php
├── CHANGELOG.md
└── README.md
/crud-definition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ahmard/quick-route/HEAD/crud-definition.png
--------------------------------------------------------------------------------
/resource-http-verbs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ahmard/quick-route/HEAD/resource-http-verbs.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /.idea
3 |
4 | /composer.lock
5 | /.phpunit.result.cache
6 | /.routes.json
7 | /.test.php
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths: ['src', 'tests']
4 | checkMissingIterableValueType: false
--------------------------------------------------------------------------------
/tests/routes-2.php:
--------------------------------------------------------------------------------
1 | name('creator');
8 | Route::patch('admin/patch', 'QuickRoute\Tests\printer');
9 | Route::delete('admin', 'QuickRoute\Tests\printer');
--------------------------------------------------------------------------------
/tests/routes-1.php:
--------------------------------------------------------------------------------
1 | name('creator');
15 | Route::patch('user/patch', 'QuickRoute\Tests\printer');
16 | Route::delete('user', 'QuickRoute\Tests\printer');
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | ./tests
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/TheRouteFactory.php:
--------------------------------------------------------------------------------
1 | enableOnRegisterEvent = $enableOnRegisterEvent;
17 | }
18 |
19 | public function onRegister(): RouteInterface
20 | {
21 | if ($this->enableOnRegisterEvent) {
22 | return parent::onRegister();
23 | }
24 |
25 | return $this;
26 | }
27 | }
--------------------------------------------------------------------------------
/tests/DelimiterTest.php:
--------------------------------------------------------------------------------
1 | group(function () {
14 | Route::get('earth', fn() => time());
15 | });
16 |
17 | $this->assertEquals('/', Getter::getDelimiter());
18 |
19 | $routeData = Getter::create()
20 | ->prefixDelimiter('.')
21 | ->get(Route::getRoutes());
22 |
23 | $this->assertEquals('.', Getter::getDelimiter());
24 | $this->assertEquals('.planets.earth', $routeData[0]['prefix']);
25 | }
26 |
27 | protected function setUp(): void
28 | {
29 | Route::restart();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ahmard/quick-route",
3 | "description": "Simple http router designed to look just like Laravel router",
4 | "minimum-stability": "stable",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Ahmad Mustapha",
9 | "email": "ahmardy@outlook.com"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=7.4",
14 | "ext-json": "*",
15 | "nikic/fast-route": "^1.3",
16 | "opis/closure": "^3.6"
17 | },
18 | "require-dev": {
19 | "phpunit/phpunit": "^9.5",
20 | "phpstan/phpstan": "^1.4",
21 | "symfony/var-dumper": "^6.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "QuickRoute\\": "src/"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "QuickRoute\\Tests\\": "tests/"
31 | }
32 | },
33 | "scripts": {
34 | "analyse": "phpstan analyse",
35 | "test": "phpunit"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ahmad Mustapha
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/CacheTest.php:
--------------------------------------------------------------------------------
1 | collectFile(__DIR__ . '/routes-1.php')
17 | ->cache(__DIR__ . '/route-cache.php')
18 | ->register();
19 |
20 | $cache = Cache::get(__DIR__ . '/route-cache.php', false);
21 |
22 | self::assertFileExists(__DIR__ . '/route-cache.php');
23 | self::assertEquals($cache, (require __DIR__ . '/route-cache.php'));
24 | }
25 |
26 | public function testClosureCaching(): void
27 | {
28 | $message = 'hello there, quick router made it.';
29 | Route::get('/', function () use ($message) {
30 | echo $message;
31 | });
32 |
33 | $collector = Collector::create()->collect()->register();
34 | $result = Dispatcher::create($collector)->dispatch('get', '/');
35 |
36 | ob_start();
37 | $result->getRoute()->getHandler()();
38 | $echoed = ob_get_contents();
39 | ob_end_clean();
40 |
41 | self::assertSame($message, $echoed);
42 | }
43 |
44 | protected function setUp(): void
45 | {
46 | Route::restart();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/CrudTest.php:
--------------------------------------------------------------------------------
1 | go();
16 |
17 | $routes = Collector::create()
18 | ->collect()
19 | ->getCollectedRoutes();
20 |
21 | self::assertSame('GET', $routes[0]['method']);
22 | self::assertSame('/users', $routes[0]['prefix']);
23 |
24 | self::assertSame('POST', $routes[1]['method']);
25 | self::assertSame('/users', $routes[1]['prefix']);
26 |
27 | self::assertSame('DELETE', $routes[2]['method']);
28 | self::assertSame('/users', $routes[2]['prefix']);
29 |
30 | self::assertSame('GET', $routes[3]['method']);
31 | self::assertSame('/users/{id}', $routes[3]['prefix']);
32 |
33 | self::assertSame('PUT', $routes[4]['method']);
34 | self::assertSame('/users/{id}', $routes[4]['prefix']);
35 |
36 | self::assertSame('DELETE', $routes[5]['method']);
37 | self::assertSame('/users/{id}', $routes[5]['prefix']);
38 | }
39 |
40 | public function testRouteDisabling(): void
41 | {
42 | Route::restart();
43 |
44 | Crud::create('/', 'Controller')
45 | ->disableIndexRoute()
46 | ->disableStoreRoute()
47 | ->disableDestroyAllRoute()
48 | ->disableShowRoute()
49 | ->disableUpdateRoute()
50 | ->disableDestroyRoute()
51 | ->go();
52 |
53 | self::assertCount(
54 | 0,
55 | Collector::create()
56 | ->collect()
57 | ->getCollectedRoutes()
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Router/DispatchResult.php:
--------------------------------------------------------------------------------
1 | dispatchResult = $dispatchResult;
27 | $this->collector = $collector;
28 | }
29 |
30 |
31 | /**
32 | * If url is found
33 | *
34 | * @return bool
35 | */
36 | public function isFound(): bool
37 | {
38 | return $this->dispatchResult[0] === FastDispatcher::FOUND;
39 | }
40 |
41 | /**
42 | * If url is not found
43 | *
44 | * @return bool
45 | */
46 | public function isNotFound(): bool
47 | {
48 | return $this->dispatchResult[0] === FastDispatcher::NOT_FOUND;
49 | }
50 |
51 | /**
52 | * If url method is not allowed
53 | *
54 | * @return bool
55 | */
56 | public function isMethodNotAllowed(): bool
57 | {
58 | return $this->dispatchResult[0] === FastDispatcher::METHOD_NOT_ALLOWED;
59 | }
60 |
61 | /**
62 | * Get dispatched url parameters
63 | * @return array|null
64 | */
65 | public function getUrlParameters(): ?array
66 | {
67 | return $this->dispatchResult[2] ?? null;
68 | }
69 |
70 | /**
71 | * Get all collected routes
72 | *
73 | * @return Collector
74 | */
75 | public function getCollector(): Collector
76 | {
77 | return $this->collector;
78 | }
79 |
80 | /**
81 | * Get found url
82 | *
83 | * @return RouteData
84 | */
85 | public function getRoute(): RouteData
86 | {
87 | return new RouteData($this->dispatchResult[1] ?? []);
88 | }
89 | }
--------------------------------------------------------------------------------
/tests/route-cache.php:
--------------------------------------------------------------------------------
1 |
3 | array(
4 | 'POST' =>
5 | array(
6 | '/user/save' =>
7 | array(
8 | 'prefix' => '/user/save',
9 | 'append' => '',
10 | 'prepend' => '',
11 | 'namespace' => '',
12 | 'name' => 'creator',
13 | 'handler' => 'QuickRoute\\Tests\\printer',
14 | 'method' => 'POST',
15 | 'middleware' => '',
16 | 'fields' =>
17 | array(),
18 | ),
19 | ),
20 | 'PATCH' =>
21 | array(
22 | '/user/patch' =>
23 | array(
24 | 'prefix' => '/user/patch',
25 | 'append' => '',
26 | 'prepend' => '',
27 | 'namespace' => '',
28 | 'name' => '',
29 | 'handler' => 'QuickRoute\\Tests\\printer',
30 | 'method' => 'PATCH',
31 | 'middleware' => '',
32 | 'fields' =>
33 | array(),
34 | ),
35 | ),
36 | 'DELETE' =>
37 | array(
38 | '/user' =>
39 | array(
40 | 'prefix' => '/user',
41 | 'append' => '',
42 | 'prepend' => '',
43 | 'namespace' => '',
44 | 'name' => '',
45 | 'handler' => 'QuickRoute\\Tests\\printer',
46 | 'method' => 'DELETE',
47 | 'middleware' => '',
48 | 'fields' =>
49 | array(),
50 | ),
51 | ),
52 | ),
53 | 1 =>
54 | array(),
55 | );
--------------------------------------------------------------------------------
/src/Router/Cache.php:
--------------------------------------------------------------------------------
1 | whereNumber('id');
18 |
19 | $expectedPrefix = Collector::create()
20 | ->collect()
21 | ->getCollectedRoutes()[0]['prefix'];
22 |
23 | assertTrue(true);
24 | assertSame('/users/{id:[0-9]+}', $expectedPrefix);
25 | }
26 |
27 | public function testWhereMethod(): void
28 | {
29 | Route::restart();
30 | Route::get('posts/{post}', 'PostController')->where('post', '[0-9]+');
31 | Route::get('users/{uid}/posts/{postId}-{postName}', 'PostController')
32 | ->whereNumber('uid')
33 | ->where([
34 | 'postId' => '[0-9]+',
35 | 'postName' => '[0-9a-zA-Z]+'
36 | ]);
37 |
38 | $routes = Collector::create()
39 | ->collect()
40 | ->getCollectedRoutes();
41 |
42 | assertSame('/posts/{post:[0-9]+}', $routes[0]['prefix']);
43 | assertSame('/users/{uid:[0-9]+}/posts/{postId:[0-9]+}-{postName:[0-9a-zA-Z]+}', $routes[1]['prefix']);
44 | }
45 |
46 | public function testOtherMethods(): void
47 | {
48 | Route::restart();
49 | Route::prefix('users')->group(function () {
50 | Route::get('/', 'UserController@list');
51 | Route::prefix('{id}')
52 | ->whereNumber('id')
53 | ->group(function () {
54 | Route::get('/', 'UserController@profile');
55 | Route::get('posts/{pid}-{pTitle}', 'PostController')
56 | ->whereNumber('pid')
57 | ->whereAlphaNumeric('pTitle');
58 | });
59 | });
60 |
61 | $routes = Collector::create()
62 | ->collect()
63 | ->getCollectedRoutes();
64 |
65 | assertSame('/users', $routes[0]['prefix']);
66 | assertSame('/users/{id:[0-9]+}', $routes[1]['prefix']);
67 | assertSame('/users/{id:[0-9]+}/posts/{pid:[0-9]+}-{pTitle:[a-zA-Z0-9]+}', $routes[2]['prefix']);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Router/RouteData.php:
--------------------------------------------------------------------------------
1 | routeData = $routeData;
14 | }
15 |
16 | /**
17 | * Get route prefix
18 | * @return string
19 | */
20 | public function getPrefix(): string
21 | {
22 | return $this->routeData['prefix'] ?? '';
23 |
24 | }
25 |
26 | /**
27 | * Get route name
28 | * @return string
29 | */
30 | public function getName(): string
31 | {
32 | return $this->routeData['name'] ?? '';
33 | }
34 |
35 | /**
36 | * Get appended route prefix
37 | * @return string
38 | */
39 | public function getAppendedPrefix(): string
40 | {
41 | return $this->routeData['append'] ?? '';
42 | }
43 |
44 | /**
45 | * Get prepended route prefix
46 | * @return string
47 | */
48 | public function getPrependedPrefix(): string
49 | {
50 | return $this->routeData['prepend'] ?? '';
51 | }
52 |
53 | /**
54 | * Get route namespace
55 | * @return string
56 | */
57 | public function getNamespace(): string
58 | {
59 | return $this->routeData['namespace'] ?? '';
60 | }
61 |
62 | /**
63 | * Get route method
64 | * @return string
65 | */
66 | public function getMethod(): string
67 | {
68 | return $this->routeData['method'] ?? '';
69 | }
70 |
71 | /**
72 | * Get route middleware
73 | * @return array
74 | */
75 | public function getMiddleware(): array
76 | {
77 | return $this->routeData['middleware'];
78 | }
79 |
80 | /**
81 | * Get route handler
82 | *
83 | * @return mixed|null
84 | */
85 | public function getHandler()
86 | {
87 | return $this->routeData['handler'] ?? null;
88 | }
89 |
90 | /**
91 | * Get route controller, this method aliases getHandler()
92 | *
93 | * @return mixed|null
94 | */
95 | public function getController()
96 | {
97 | return $this->routeData['handler'] ?? null;
98 | }
99 |
100 | /**
101 | * Get route fields
102 | * @return array
103 | */
104 | public function getFields(): array
105 | {
106 | return $this->routeData['fields'] ?? [];
107 | }
108 |
109 | /**
110 | * Gets regular expressions associated with this route
111 | *
112 | * @return array
113 | */
114 | public function getRegExp(): array
115 | {
116 | return $this->routeData['parameterTypes'] ?? [];
117 | }
118 |
119 | /**
120 | * @inheritDoc
121 | */
122 | public function jsonSerialize(): array
123 | {
124 | return $this->getData();
125 | }
126 |
127 | /**
128 | * Get route data as array
129 | * @return array
130 | */
131 | public function getData(): array
132 | {
133 | return $this->routeData;
134 | }
135 | }
--------------------------------------------------------------------------------
/src/Route.php:
--------------------------------------------------------------------------------
1 | $name(...$args);
51 | }
52 |
53 | /**
54 | * Add route class to list of routes
55 | *
56 | * @param TheRoute $route
57 | */
58 | public static function push(TheRoute $route): void
59 | {
60 | self::$called[] = $route;
61 | }
62 |
63 | /**
64 | * Create fresh router
65 | *
66 | * @return TheRoute
67 | */
68 | public static function create(): TheRoute
69 | {
70 | $route = new TheRoute();
71 | self::push($route);
72 | return $route;
73 | }
74 |
75 | /**
76 | * Clear previously collected routes
77 | */
78 | public static function restart(): void
79 | {
80 | self::$called = [];
81 | }
82 |
83 | /**
84 | * Get all registered routers
85 | *
86 | * @return TheRoute[]
87 | */
88 | public static function getRoutes(): array
89 | {
90 | return self::$called;
91 | }
92 | }
--------------------------------------------------------------------------------
/tests/DispatcherTest.php:
--------------------------------------------------------------------------------
1 | collect();
25 | $routeData = $collector->getCollectedRoutes()[0];
26 | $collector->register();
27 | $result = Dispatcher::create($collector)->dispatch('get', '/');
28 |
29 | self::assertSame($routeData, $result->getRoute()->getData());
30 | }
31 |
32 | public function testUnregisteredCollector(): void
33 | {
34 | Route::restart();
35 | Route::get('/', 'hello');
36 |
37 | $collector = Collector::create()->collect();
38 | $routeData = $collector->getCollectedRoutes()[0];
39 | $result = Dispatcher::create($collector)->dispatch('get', '/');
40 | self::assertSame($routeData, $result->getRoute()->getData());
41 | }
42 |
43 | public function testUsedDispatcher(): void
44 | {
45 | Route::restart();
46 | Route::get('/', 'hello');
47 |
48 | $collector = Collector::create()->collect()->register();
49 | $routeData = $collector->getCollectedRoutes()[0];
50 |
51 | $result = Dispatcher::create($collector)
52 | ->setDispatcher(CharCountBased::class)
53 | ->dispatch('get', '/');
54 |
55 | self::assertSame($routeData, $result->getRoute()->getData());
56 | }
57 |
58 | public function testTrailingSlash(): void
59 | {
60 | Route::restart();
61 | Route::get('/user/admin/', 'hello');
62 |
63 | $collector = Collector::create()->collect()->register();
64 |
65 | $result = Dispatcher::create($collector)
66 | ->dispatch('get', '/user/admin');
67 |
68 | self::assertTrue($result->isFound());
69 | }
70 |
71 | public function testDoubleForwardSlash(): void
72 | {
73 | Route::restart();
74 | Route::get('/user/admin/', 'hello');
75 |
76 | $collector = Collector::create()->collect()->register();
77 |
78 | $result = Dispatcher::create($collector)
79 | ->dispatch('get', '/user//admin');
80 |
81 | self::assertTrue($result->isFound());
82 | }
83 |
84 | public function testBadAmpersand(): void
85 | {
86 | Route::restart();
87 | Route::get('/amp', 'hello');
88 |
89 | $collector = Collector::create()->collect()->register();
90 |
91 | $result = Dispatcher::create($collector)
92 | ->dispatch('get', '/amp&a=3&r=4');
93 |
94 | self::assertTrue($result->isFound());
95 | }
96 |
97 | public function testCollectorMethods(): void
98 | {
99 | Route::restart();
100 | Route::get('users/profile', 'Controller@index');
101 | $result = Dispatcher::collectRoutes()
102 | ->dispatch('get', 'users/profile');
103 |
104 | $result1 = Dispatcher::collectRoutesFile(__DIR__ . '/routes-1.php')
105 | ->dispatch('post', '/user/save');
106 |
107 | self::assertSame('/users/profile', $result->getRoute()->getPrefix());
108 | self::assertSame('/user/save', $result1->getRoute()->getPrefix());
109 | }
110 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # QuickRoute ChangeLog
2 |
3 | ## v3.6 to v3.7
4 | ### New Methods
5 | - RouteInterface::matchAny(array $methods, array $paths, $handler): RouteInterface - Generate multiple routes using multiple method and path but single handle
6 | - Collector::route(string $routeName): ?array - This is to find route by its name
7 | - Collector::uri(string $routeName): ?string - Generate route uri using route's name
8 | - RouteData::getRegExp() - This will return regular expression defined within the route's prefix/pth
9 | - RouteInterface::match() will now add route name based on the used request method
10 | - RouteData::getController() added to alias getHandler()
11 |
12 | #### RouteInterface::matchAny()
13 | ```php
14 | use QuickRoute\Route;
15 |
16 | Route::matchAny(
17 | ['get', 'post'],
18 | ['/customer/login', '/admin/login'],
19 | 'MainController@index'
20 | );
21 |
22 | //Which is equivalent to:
23 | Route::get('/customer/login', 'MainController@index');
24 | Route::post('/customer/login', 'MainController@index');
25 | Route::get('/admin/login', 'MainController@index');
26 | Route::post('/admin/login', 'MainController@index');
27 | ```
28 | #### Finding route & generating route uri
29 |
30 | ```php
31 | use QuickRoute\Route;
32 | use QuickRoute\Router\Collector;
33 |
34 | Route::get('/users', 'Controller@method')->name('users.index');
35 |
36 | $collector = Collector::create()->collect();
37 | echo $collector->uri('users.index'); // => /users
38 | $collector->route('users.index'); // => Instance of QuickRoute\Route\RouteData
39 | ```
40 |
41 | #### Route::match() with route name
42 |
43 | ```php
44 | use QuickRoute\Route;
45 |
46 | Route::match(['get', 'post'], 'login', 'AuthController@login')->name('login.');
47 |
48 | //Will generate below routes
49 | Route::get('login', 'AuthController@login')->name('login.get');
50 | Route::post('login', 'AuthController@login')->name('login.post');
51 | ```
52 |
53 | ## v3.5 to v3.6
54 | ### New Methods
55 | - RouteInterface::where(string|array $param, ?string $regExp = null): RouteInterface
56 | - RouteInterface::matchAny(array $methods, array $paths, $handler): RouteInterface
57 |
58 | ```php
59 | use QuickRoute\Route;
60 |
61 | Route::get('/users/{id}', 'a')->where('id', '[0-9]+');
62 | Route::get('/users/{user}/posts/{post}', 'Ctrl@method')->where([
63 | 'user' => '[a-zA-Z]+',
64 | 'post' => '[0-9]+'
65 | ]);
66 | ```
67 |
68 | ### Bug fixes
69 | - Regular expression bug related to whereNumber(), whereAlpha(), whereAlphanumeric() methods has been fixed
70 |
71 | ### Changes
72 | - RouteInterface::resource() gets one additional parameter
73 |
You can now provide id parameter name
74 |
75 | ```php
76 | use QuickRoute\Route;
77 |
78 | Route::resource('/users', 'UserController', 'userId', true);
79 | // /users/{userId:[0-9]+}
80 | ```
81 |
82 | ## v3.4 to v3.5
83 | ### New Methods
84 | - RouteInterface::whereNumber(string $param): RouteInterface
85 | - RouteInterface::whereAlpha(string $param): RouteInterface
86 | - RouteInterface::whereAlphanumeric(string $param): RouteInterface
87 | - Dispatcher::collectRoutes(array $routesInfo = []): Dispatcher
88 | - Dispatcher::collectRoutesFile(string $filePath, array $routesInfo = []): Dispatcher
89 |
90 | ```php
91 | use QuickRoute\Route;
92 | use QuickRoute\Router\Dispatcher;
93 |
94 | Route::get('users/{id}', 'Controller@index')->whereNumber('id');
95 | Route::get('users/{name}', 'Controller@profile')->whereAlpha('name');
96 | Route::get('users/{username}', 'Controller@profile')->whereAlphaNumeric('username');
97 |
98 | //Collect routes
99 | Dispatcher::collectRoutes()->dispatch('get', '/');
100 | //Collect routes in file
101 | Dispatcher::collectRoutesFile('routes.php')->dispatch('get', '/');
102 | ```
--------------------------------------------------------------------------------
/tests/CollectorTest.php:
--------------------------------------------------------------------------------
1 | print "Hello world");
16 | Route::get('hello/planet', fn() => print "Hello planet");
17 | $collector = Collector::create()
18 | ->collect();
19 |
20 | $fileCollector = Collector::create()
21 | ->collectFile(__DIR__ . '/routes-1.php');
22 |
23 | $this->assertCount(2, $collector->getCollectedRoutes());
24 | $this->assertCount(3, $fileCollector->getCollectedRoutes());
25 | }
26 |
27 | public function testDispatcher(): void
28 | {
29 | $collector = Collector::create()
30 | ->collectFile(__DIR__ . '/routes-1.php');
31 |
32 | $collectedRoutes = $collector->getCollectedRoutes();
33 |
34 | $collector->register();
35 |
36 | $dispatchResult = Dispatcher::create($collector)
37 | ->dispatch('POST', 'user/save');
38 | self::assertTrue($dispatchResult->isFound());
39 | self::assertEquals($collectedRoutes[0], $dispatchResult->getRoute()->getData());
40 | self::assertEquals('creator', $dispatchResult->getRoute()->getName());
41 |
42 | $dispatchResult2 = Dispatcher::create($collector)
43 | ->dispatch('GET', 'user/list');
44 | self::assertTrue($dispatchResult2->isNotFound());
45 |
46 | $dispatchResult3 = Dispatcher::create($collector)
47 | ->dispatch('GET', 'user');
48 | self::assertTrue($dispatchResult3->isMethodNotAllowed());
49 | }
50 |
51 | public function testIsRegisterMethod(): void
52 | {
53 | $collector = Collector::create()
54 | ->collectFile(__DIR__ . '/routes-1.php');
55 |
56 | self::assertFalse($collector->isRegistered());
57 |
58 | $collector->register();
59 |
60 | self::assertTrue($collector->isRegistered());
61 | }
62 |
63 | public function testMultipleRouteFileCollection(): void
64 | {
65 | $collector = Collector::create()
66 | ->collectFile(__DIR__ . '/routes-1.php')
67 | ->collectFile(__DIR__ . '/routes-2.php');
68 |
69 | $dispatchResult1 = Dispatcher::create($collector)
70 | ->dispatch('POST', 'user/save');
71 |
72 | $dispatchResult2 = Dispatcher::create($collector)
73 | ->dispatch('POST', 'admin/save');
74 |
75 | self::assertTrue($dispatchResult1->isFound());
76 | self::assertTrue($dispatchResult2->isFound());
77 | }
78 |
79 | public function testHelpers(): void
80 | {
81 | Route::restart();
82 | Route::get('/', 'Controller@method')->name('home');
83 | Route::get('/users/{id}/{name}', 'HelloController')
84 | ->whereAlpha('id')
85 | ->name('user.info');
86 |
87 | $result = Dispatcher::collectRoutes()->dispatch('get', '/');
88 | $uri = $result->getCollector()->uri('user.info', [
89 | 'id' => 1,
90 | 'name' => 'ahmard'
91 | ]);
92 |
93 | $route = $result->getCollector()->route('home');
94 |
95 | self::assertTrue($result->isFound());
96 | self::assertInstanceOf(RouteData::class, $route);
97 | /**@phpstan-ignore-next-line**/
98 | self::assertSame('Controller@method', $route->getHandler() ?? null);
99 | self::assertSame('/users/1/ahmard', $uri);
100 | }
101 |
102 | protected function setUp(): void
103 | {
104 | Route::restart();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Router/Dispatcher.php:
--------------------------------------------------------------------------------
1 | collector = $collector;
18 | }
19 |
20 | /**
21 | * Collect routes defined above or in included file
22 | *
23 | * @param array $routesInfo
24 | * @return Dispatcher
25 | */
26 | public static function collectRoutes(array $routesInfo = []): Dispatcher
27 | {
28 | return self::create(Collector::create()->collect($routesInfo));
29 | }
30 |
31 | /**
32 | * Creates dispatcher instance
33 | *
34 | * @param Collector $collector
35 | * @return Dispatcher
36 | */
37 | public static function create(Collector $collector): Dispatcher
38 | {
39 | return new self($collector);
40 | }
41 |
42 | /**
43 | * Collect routes defined in a file
44 | *
45 | * @param string $filePath
46 | * @param array $routesInfo
47 | * @return Dispatcher
48 | */
49 | public static function collectRoutesFile(string $filePath, array $routesInfo = []): Dispatcher
50 | {
51 | return self::create(Collector::create()->collectFile($filePath, $routesInfo));
52 | }
53 |
54 | /**
55 | * Dispatch url routing
56 | * @param string $method Route method - It will be converted to uppercase
57 | * @param string $path Route url path - All data passed to url parameter after "?" will be discarded
58 | * @return DispatchResult
59 | */
60 | public function dispatch(string $method, string $path): DispatchResult
61 | {
62 | $lengthPath = strlen($path) - 1;
63 |
64 | //Make url convertible
65 | if (false !== $pos = strpos($path, '?')) {
66 | $path = substr($path, 0, $pos);
67 | }
68 |
69 | //invalid & in url at ? position
70 | if (false !== $pos = strpos($path, '&')) {
71 | $path = substr($path, 0, $pos);
72 | }
73 |
74 | $path = str_replace('//', '/', $path);
75 | $path = rawurldecode($path);
76 |
77 | //Remove trailing forward slash
78 | if (($lengthPath > 0) && substr($path, $lengthPath, 1) == Getter::getDelimiter()) {
79 | $path = substr($path, 0, $lengthPath);
80 | }
81 |
82 | if (substr($path, 0, 1) != Getter::getDelimiter()) {
83 | $path = Getter::getDelimiter() . $path;
84 | }
85 |
86 | $urlData = $this->createDispatcher()
87 | ->dispatch(strtoupper($method), $path);
88 |
89 | return new DispatchResult($urlData, $this->collector);
90 | }
91 |
92 | /**
93 | * @return FastDispatcher
94 | */
95 | private function createDispatcher(): FastDispatcher
96 | {
97 | if (!isset($this->dispatcher)) {
98 | $this->dispatcher = GroupCountBased::class;
99 | }
100 |
101 | //Register collector if it is not registered
102 | if (!$this->collector->isRegistered()) {
103 | $this->collector->register();
104 | }
105 |
106 | $dispatcher = $this->dispatcher;
107 | $routeData = $this->collector->getFastRouteData();
108 |
109 | return (new $dispatcher($routeData));
110 | }
111 |
112 | /**
113 | * Set your own dispatcher
114 | * @param string $dispatcher A class namespace implementing \FastRoute\Dispatcher
115 | * @return Dispatcher
116 | */
117 | public function setDispatcher(string $dispatcher): Dispatcher
118 | {
119 | $this->dispatcher = $dispatcher;
120 | return $this;
121 | }
122 | }
--------------------------------------------------------------------------------
/src/Crud.php:
--------------------------------------------------------------------------------
1 | uri = $uri;
34 | $this->controller = $controller;
35 | }
36 |
37 | /**
38 | * Set id parameter name
39 | *
40 | * @param string $name parameter name
41 | * @param string|null $regExp regular expression
42 | * @return $this
43 | */
44 | public function parameter(string $name, ?string $regExp = null): Crud
45 | {
46 | $this->parameterName = $name;
47 |
48 | if ($regExp) {
49 | $this->parameterRegExp = $regExp;
50 | }
51 |
52 | return $this;
53 | }
54 |
55 | /**
56 | * Mark parameter as of numeric type
57 | *
58 | * @param string $name parameter name
59 | * @return $this
60 | */
61 | public function numericParameter(string $name = 'id'): Crud
62 | {
63 | return $this->parameter($name, ':[0-9]+');
64 | }
65 |
66 | /**
67 | * Mark parameter as of alphanumeric type
68 | *
69 | * @param string $name
70 | * @return $this
71 | */
72 | public function alphabeticParameter(string $name = 'id'): Crud
73 | {
74 | return $this->parameter($name, ':[a-zA-Z]+');
75 | }
76 |
77 | /**
78 | * Mark parameter as of alphanumeric type
79 | *
80 | * @param string $name
81 | * @return $this
82 | */
83 | public function alphaNumericParameter(string $name = 'id'): Crud
84 | {
85 | return $this->parameter($name, ':[a-zA-Z]+');
86 | }
87 |
88 | /**
89 | * Perform the routes creation
90 | */
91 | public function go(): void
92 | {
93 | $idParam = '{' . "{$this->parameterName}{$this->parameterRegExp}" . '}';
94 |
95 | // GET /whatever
96 | if (!in_array('index', $this->disabledRoutes)) {
97 | $route = new TheRoute();
98 | $route->get($this->uri, [$this->controller, 'index'])->name('index');
99 | Route::push($route);
100 | }
101 |
102 | // POST /whatever
103 | if (!in_array('store', $this->disabledRoutes)) {
104 | $route = new TheRoute();
105 | $route->post($this->uri, [$this->controller, 'store'])->name('store');
106 | Route::push($route);
107 | }
108 |
109 | // DELETE /whatever
110 | if (!in_array('destroy_all', $this->disabledRoutes)) {
111 | $route = new TheRoute();
112 | $route->delete($this->uri, [$this->controller, 'destroyAll'])->name('destroy_all');
113 | Route::push($route);
114 | }
115 |
116 | // GET /whatever/{$id}
117 | if (!in_array('show', $this->disabledRoutes)) {
118 | $route = new TheRoute();
119 | $route->get("$this->uri/$idParam", [$this->controller, 'show'])->name('show');
120 | Route::push($route);
121 | }
122 |
123 | // PATCH /whatever/{$id}
124 | if (!in_array('update', $this->disabledRoutes)) {
125 | $route = new TheRoute();
126 | $route->put("$this->uri/$idParam", [$this->controller, 'update'])->name('update');
127 | Route::push($route);
128 | }
129 |
130 | // DELETE /whatever/{$id}
131 | if (!in_array('destroy', $this->disabledRoutes)) {
132 | $route = new TheRoute();
133 | $route->delete("$this->uri/$idParam", [$this->controller, 'destroy'])->name('destroy');
134 | Route::push($route);
135 | }
136 | }
137 |
138 | /**
139 | * This will prevent the get all route from generating
140 | *
141 | * @return $this
142 | */
143 | public function disableIndexRoute(): Crud
144 | {
145 | $this->disabledRoutes[] = 'index';
146 | return $this;
147 | }
148 |
149 | /**
150 | * This will prevent the create route from generating
151 | *
152 | * @return $this
153 | */
154 | public function disableStoreRoute(): Crud
155 | {
156 | $this->disabledRoutes[] = 'store';
157 | return $this;
158 | }
159 |
160 | /**
161 | * This will prevent the destroy all route from generating
162 | *
163 | * @return $this
164 | */
165 | public function disableDestroyAllRoute(): Crud
166 | {
167 | $this->disabledRoutes[] = 'destroy_all';
168 | return $this;
169 | }
170 |
171 | /**
172 | * This will prevent the get one route from generating
173 | *
174 | * @return $this
175 | */
176 | public function disableShowRoute(): Crud
177 | {
178 | $this->disabledRoutes[] = 'show';
179 | return $this;
180 | }
181 |
182 | /**
183 | * This will prevent the update route from generating
184 | *
185 | * @return $this
186 | */
187 | public function disableUpdateRoute(): Crud
188 | {
189 | $this->disabledRoutes[] = 'update';
190 | return $this;
191 | }
192 |
193 | /**
194 | * This will prevent the delete one route from generating
195 | *
196 | * @return $this
197 | */
198 | public function disableDestroyRoute(): Crud
199 | {
200 | $this->disabledRoutes[] = 'destroy';
201 | return $this;
202 | }
203 | }
--------------------------------------------------------------------------------
/src/RouteInterface.php:
--------------------------------------------------------------------------------
1 | willCollect = true;
101 | $this->collectableRoutes[] = [
102 | 'file' => $filePath,
103 | 'data' => $routesInfo,
104 | ];
105 |
106 | return $this;
107 | }
108 |
109 | /**
110 | * Collect routes defined above or in included file
111 | *
112 | * @param array $routesInfo
113 | * @return Collector
114 | */
115 | public function collect(array $routesInfo = []): Collector
116 | {
117 | $this->willCollect = true;
118 | $this->collectableRoutes[] = [
119 | 'data' => $routesInfo,
120 | ];
121 |
122 | return $this;
123 | }
124 |
125 | /**
126 | * Cache this group
127 | *
128 | * @param string $cacheFile Location to cache file
129 | * @param bool $hasClosures Indicates that routes has closures in their handlers.
130 | * This will tell caching lib to not look closures.
131 | * @return $this
132 | */
133 | public function cache(string $cacheFile, bool $hasClosures = false): self
134 | {
135 | $this->cacheFile = $cacheFile;
136 | $this->routesHasClosures = $hasClosures;
137 |
138 | return $this;
139 | }
140 |
141 | /**
142 | * Set custom route prefix delimiter
143 | *
144 | * @param string $delimiter
145 | * @return $this
146 | */
147 | public function prefixDelimiter(string $delimiter): self
148 | {
149 | $this->delimiter = $delimiter;
150 | return $this;
151 | }
152 |
153 | /**
154 | * Register routes to FastRoute
155 | *
156 | * @return $this
157 | */
158 | public function register(): self
159 | {
160 | $this->doCollectRoutes();
161 | $rootFastCollector = $this->getFastRouteCollector(true);
162 |
163 | if (!empty($this->cachedRoutes)) {
164 | $this->fastRouteData = $this->cachedRoutes;
165 | return $this;
166 | }
167 |
168 | foreach ($this->collectedRoutes as $collectedRoute) {
169 | //Register to root collector
170 | $rootFastCollector->addRoute($collectedRoute['method'], $collectedRoute['prefix'], $collectedRoute);
171 | }
172 |
173 | $this->fastRouteData = $rootFastCollector->getData();
174 |
175 | if (empty($this->cachedRoutes) && '' != $this->cacheFile) {
176 | Cache::create($this->cacheFile, $this->fastRouteData);
177 | }
178 |
179 | $this->isRegistered = true;
180 |
181 | return $this;
182 | }
183 |
184 | /**
185 | * Perform route collection
186 | *
187 | * @return void
188 | */
189 | private function doCollectRoutes(): void
190 | {
191 | if (!$this->willCollect) {
192 | return;
193 | }
194 |
195 | if ('' != $this->cacheFile) {
196 | $cachedVersion = Cache::get($this->cacheFile, $this->routesHasClosures);
197 | }
198 |
199 | if (!empty($cachedVersion)) {
200 | $this->cachedRoutes = $cachedVersion;
201 | return;
202 | }
203 |
204 | foreach ($this->collectableRoutes as $collectableRoute) {
205 | $collectableFile = $collectableRoute['file'] ?? null;
206 | if (isset($collectableFile)) {
207 | Route::restart();
208 | require $collectableFile;
209 | //Store collected routes
210 | }
211 | $this->collectedRoutes = array_merge(
212 | $this->collectedRoutes,
213 | Getter::create()
214 | ->prefixDelimiter($this->delimiter)
215 | ->get(
216 | Route::getRoutes(),
217 | $collectableRoute['data']
218 | )
219 | );
220 | }
221 |
222 | $this->willCollect = false;
223 | }
224 |
225 | /**
226 | * Get FastRoute's route collector
227 | *
228 | * @param bool $createNew
229 | * @return FastRouteCollector
230 | */
231 | public function getFastRouteCollector(bool $createNew = false): FastRouteCollector
232 | {
233 | if (isset($this->collector)) {
234 | if ($createNew) {
235 | $this->collector = new FastRouteCollector(new Std(), new GroupCountBased());
236 | }
237 | } else {
238 | $this->collector = new FastRouteCollector(new Std(), new GroupCountBased());
239 | }
240 |
241 | return $this->collector;
242 | }
243 |
244 | /**
245 | * Get collected routes, array of routes
246 | *
247 | * @return array[]
248 | */
249 | public function getCollectedRoutes(): array
250 | {
251 | $this->doCollectRoutes();
252 | return $this->collectedRoutes;
253 | }
254 |
255 | /**
256 | * @return array[]
257 | */
258 | public function getCachedRoutes(): array
259 | {
260 | $this->doCollectRoutes();
261 | return $this->cachedRoutes;
262 | }
263 |
264 | /**
265 | * Get computed route
266 | *
267 | * @return array[]
268 | */
269 | public function getFastRouteData(): array
270 | {
271 | return $this->fastRouteData;
272 | }
273 |
274 | /**
275 | * Checks whether this collector has been registered
276 | *
277 | * @return bool
278 | */
279 | public function isRegistered(): bool
280 | {
281 | return $this->isRegistered;
282 | }
283 |
284 |
285 | /**
286 | * Find route by name
287 | *
288 | * @param string $routeName
289 | * @return RouteData|null
290 | */
291 | public function route(string $routeName): ?RouteData
292 | {
293 | $this->doCollectRoutes();
294 | foreach ($this->collectedRoutes as $collectedRoute) {
295 | if ($routeName == $collectedRoute['name']) {
296 | return new RouteData($collectedRoute);
297 | }
298 | }
299 |
300 | return null;
301 | }
302 |
303 | /**
304 | * Generate route http uri using route's name
305 | *
306 | * @param string $routeName
307 | * @param array $routeParams an array of [key => value] of route parameters
308 | * @return string|null
309 | */
310 | public function uri(string $routeName, array $routeParams = []): ?string
311 | {
312 | $foundRoute = $this->route($routeName);
313 | if (!$foundRoute) return null;
314 |
315 | $prefix = $foundRoute->getPrefix() ?? null;
316 | if (!$prefix) return null;
317 |
318 | return $this->replaceParamWithValue($prefix, $routeParams);
319 | }
320 |
321 | protected function replaceParamWithValue(string $prefix, array $params): string
322 | {
323 | preg_match_all("@{([0-9a-zA-Z]+):?.*?\+?}@", $prefix, $matchedParams);
324 |
325 | for ($i = 0; $i < count($matchedParams[0]); $i++) {
326 | $paramValue = $params[$matchedParams[1][$i]] ?? null;
327 | if (null == $paramValue) {
328 | throw new InvalidArgumentException("Missing route parameter value for \"{$matchedParams[1][$i]}\"");
329 | }
330 | $prefix = str_replace($matchedParams[0][$i], $params[$matchedParams[1][$i]], $prefix);
331 | }
332 |
333 | return $prefix;
334 | }
335 |
336 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QuickRoute
2 | An elegant http router built on top of [FastRoute](https://github.com/nikic/FastRoute) to provide more easy of use.
3 |
4 | ## Upgrade Guide
5 | Check [ChangeLog](CHANGELOG.md) file
6 |
7 | ## Installation
8 | ```bash
9 | composer require ahmard/quick-route
10 | ```
11 |
12 | ## Usage
13 |
14 | Simple example
15 |
16 | ```php
17 | use QuickRoute\Route;
18 | use QuickRoute\Router\Dispatcher;
19 |
20 | require('vendor/autoload.php');
21 |
22 | Route::get('/', function () {
23 | echo 'Hello world';
24 | });
25 |
26 | $method = $_SERVER['REQUEST_METHOD'];
27 | $path = $_SERVER['REQUEST_URI'];
28 |
29 | //create route dispatcher
30 | $dispatcher = Dispatcher::collectRoutes()
31 | ->dispatch($method, $path);
32 |
33 | //determine dispatch result
34 | switch (true) {
35 | case $dispatcher->isFound():
36 | $controller = $dispatcher->getRoute()->getController();
37 | $controller($dispatcher->getUrlParameters());
38 | break;
39 | case $dispatcher->isNotFound():
40 | echo "Page not found";
41 | break;
42 | case $dispatcher->isMethodNotAllowed():
43 | echo "Request method not allowed";
44 | break;
45 | }
46 | ```
47 |
48 | #### Controller-like example
49 |
50 | ```php
51 | use QuickRoute\Route;
52 |
53 | Route::get('/home', 'MainController@home');
54 | ```
55 |
56 | #### Advance usage
57 |
58 | ```php
59 | use QuickRoute\Route;
60 |
61 | Route::prefix('user')->name('user.')
62 | ->namespace('User')
63 | ->middleware('UserMiddleware')
64 | ->group(function (){
65 | Route::get('profile', 'UserController@profile');
66 | Route::put('update', 'UserController@update');
67 | });
68 | ```
69 |
70 | #### More Advance Usage
71 |
72 | ```php
73 | use QuickRoute\Route;
74 |
75 | Route::prefix('user')
76 | ->prepend('api')
77 | ->append('{token}')
78 | ->middleware('UserMiddleware')
79 | ->group(function (){
80 | Route::get('profile', 'UserController@profile');
81 | Route::put('update', 'UserController@update');
82 | });
83 |
84 | // => /api/user/{token}
85 | ```
86 |
87 | #### Defining route param types
88 | ```php
89 | use QuickRoute\Route;
90 |
91 | // id => must be number
92 | Route::get('users/{id}', 'Controller@index')->whereNumber('id');
93 | // name => must be alphabetic
94 | Route::get('users/{name}', 'Controller@profile')->whereAlpha('name');
95 | // username => must be alphanumeric
96 | Route::get('users/{username}', 'Controller@profile')->whereAlphaNumeric('username');
97 |
98 | // Manually provide regular expression pattern to match parameter with
99 | Route::get('/users/{id}', 'a')->where('id', '[0-9]+');
100 | Route::get('/users/{user}/posts/{post}', 'Ctrl@method')->where([
101 | 'user' => '[a-zA-Z]+',
102 | 'post' => '[0-9]+'
103 | ]);
104 | ```
105 |
106 | #### Route Fields
107 | Fields help to add more description to route or group of routes
108 |
109 | ```php
110 | use QuickRoute\Route;
111 |
112 | Route::prefix('user')
113 | ->middleware('User')
114 | ->addField('specie', 'human')
115 | ->group(function (){
116 | Route::get('type', 'admin')->addField('permissions', 'all');
117 | Route::get('g', fn() => print('Hello world'));
118 | });
119 |
120 | ```
121 |
122 | #### Route::match()
123 | ```php
124 | use QuickRoute\Route;
125 | use QuickRoute\Router\Collector;
126 | use QuickRoute\Router\Dispatcher;
127 |
128 | require 'vendor/autoload.php';
129 |
130 | $controller = fn() => print time();
131 | Route::match(['get', 'post'], '/user', $controller)
132 | ->middleware('auth')
133 | ->namespace('App')
134 | ->name('home');
135 |
136 | $collector = Collector::create()->collect();
137 |
138 | $dispatchResult = Dispatcher::create($collector)
139 | ->dispatch('get', '/user/hello');
140 |
141 | var_export($dispatchResult->getRoute());
142 | ```
143 |
144 | #### Route::match() with named routes
145 | ```php
146 | use QuickRoute\Route;
147 |
148 | Route::match(['get', 'post'], 'login', 'AuthController@login')->name('login.');
149 |
150 | //Will generate below routes
151 | Route::get('login', 'AuthController@login')->name('login.get');
152 | Route::post('login', 'AuthController@login')->name('login.post');
153 | ```
154 |
155 | #### Route::any()
156 |
157 | ```php
158 | use QuickRoute\Route;
159 | use QuickRoute\Router\Collector;
160 | use QuickRoute\Router\Dispatcher;
161 |
162 | $controller = fn() => print time();
163 |
164 | Route::any(['/login', '/admin/login'], 'get', $controller);
165 |
166 | $collector = Collector::create()->collect();
167 |
168 | $dispatchResult1 = Dispatcher::create($collector)
169 | ->dispatch('get', '/login');
170 |
171 | $dispatchResult2 = Dispatcher::create($collector)
172 | ->dispatch('get', '/admin/login');
173 | ```
174 |
175 | #### Route::matchAny()
176 |
177 | ```php
178 | use QuickRoute\Route;
179 |
180 | Route::matchAny(
181 | ['get', 'post'],
182 | ['/customer/login', '/admin/login'],
183 | 'MainController@index'
184 | );
185 |
186 | //Which is equivalent to:
187 | Route::get('/customer/login', 'MainController@index');
188 | Route::post('/customer/login', 'MainController@index');
189 | Route::get('/admin/login', 'MainController@index');
190 | Route::post('/admin/login', 'MainController@index');
191 | ```
192 |
193 | #### Route::resource()
194 |
195 | ```php
196 | use QuickRoute\Route;
197 |
198 | Route::resource('photos', 'App\Http\Controller\PhotoController');
199 | ```
200 | Code above will produce below routes
201 | 
202 |
203 | #### Crud::create()
204 |
205 | ```php
206 | use QuickRoute\Crud;
207 |
208 | Crud::create('/', 'Controller')->go();
209 | ```
210 | Code above will produce below routes
211 | 
212 |
213 | **Why not use Route::resource()?**
214 | Crud creator generates 6 routes, one of the routes which deletes all record in the endpoint.
215 | With Crud creator you can choose which routes to create or not.
216 |
217 | ```php
218 | use QuickRoute\Crud;
219 |
220 | //Disabling route creation
221 | Crud::create('/', 'Controller')
222 | ->disableIndexRoute()
223 | ->disableStoreRoute()
224 | ->disableDestroyAllRoute()
225 | ->disableShowRoute()
226 | ->disableUpdateRoute()
227 | ->disableDestroyRoute()
228 | ->go();
229 |
230 | //Specifying custom route parameter name
231 | Crud::create('/', 'Controller')->parameter('userId');
232 |
233 | //Specify parameter type
234 | Crud::create('/', 'Controller')->numericParameter();
235 | Crud::create('/', 'Controller')->alphabeticParameter();
236 | Crud::create('/', 'Controller')->alphaNumericParameter();
237 | ```
238 |
239 | #### Routes as configuration
240 |
241 | ```php
242 | //routes.php
243 | use QuickRoute\Route;
244 |
245 | Route::get('/', 'MainController@index');
246 | Route::get('/help', 'MainController@help');
247 |
248 |
249 | //server.php
250 | use QuickRoute\Router\Collector;
251 |
252 | $collector = Collector::create()
253 | ->collectFile('routes.php')
254 | ->register();
255 |
256 | $routes = $collector->getCollectedRoutes();
257 | ```
258 |
259 | #### Caching
260 | Cache routes so that they don't have to be collected every time.
261 |
262 | ```php
263 | use QuickRoute\Router\Collector;
264 |
265 | $collector = Collector::create()
266 | ->collectFile('routes.php')
267 | ->cache('path/to/save/cache.php', false)
268 | ->register();
269 |
270 | $routes = $collector->getCollectedRoutes();
271 | ```
272 |
273 | Caching routes with closure
274 |
275 | ```php
276 | use QuickRoute\Route;
277 | use QuickRoute\Router\Collector;
278 |
279 | Route::get('/', function (){
280 | echo uniqid();
281 | });
282 |
283 | $collector = Collector::create()
284 | ->collect()
285 | ->cache('path/to/save/cache.php', true)
286 | ->register();
287 |
288 | $routes = $collector->getCollectedRoutes();
289 | ```
290 | **Note that you must specify that your routes contains closure**
291 |
292 |
293 | #### Passing Default Data
294 | You can alternatively pass data to be prepended to all routes.
295 |
296 | Cached routes must be cleared manually after setting/updating default route data.
297 |
298 | ```php
299 | use QuickRoute\Router\Collector;
300 |
301 | $collector = Collector::create();
302 | $collector->collectFile('api-routes.php', [
303 | 'prefix' => 'api',
304 | 'name' => 'api.',
305 | 'namespace' => 'Api\\'
306 | ]);
307 | $collector->register();
308 | ```
309 |
310 | #### Changing Delimiter
311 | For usage outside of web context, a function to change default delimiter which is "**/**" has been provided.
312 |
313 | ```php
314 | use QuickRoute\Route;
315 | use QuickRoute\Router\Collector;
316 | use QuickRoute\Router\Dispatcher;
317 |
318 | require 'vendor/autoload.php';
319 |
320 | Route::prefix('hello')
321 | ->group(function () {
322 | Route::get('world', fn() => print('Hello World'));
323 | });
324 |
325 | $collector = Collector::create()
326 | ->prefixDelimiter('.')
327 | ->collect()
328 | ->register();
329 |
330 | $dispatchResult = Dispatcher::create($collector)
331 | ->dispatch('get', 'hello.world');
332 |
333 | var_export($dispatchResult);
334 | ```
335 |
336 | #### Finding route & generating route uri
337 |
338 | ```php
339 | use QuickRoute\Route;
340 | use QuickRoute\Router\Collector;
341 |
342 | Route::get('/users', 'Controller@method')->name('users.index');
343 |
344 | $collector = Collector::create()->collect();
345 | echo $collector->uri('users.index'); // => /users
346 | $collector->route('users.index'); // => Instance of QuickRoute\Route\RouteData
347 | ```
348 |
349 | #### Note
350 | - You must be careful when using **Collector::collect()** and **Collector::collectFile()** together,
351 | as collectFile method will clear previously collected routes before it starts collecting.
352 | Make sure that you call **Collector::collect()** first, before calling **Collector::collectFile()**.
353 |
354 |
355 | ## Licence
356 | _Route http verbs image is owned by [Riptutorial](https://riptutorial.com)_.
357 |
358 | **QuickRoute** is **MIT** licenced.
359 |
--------------------------------------------------------------------------------
/src/Router/Getter.php:
--------------------------------------------------------------------------------
1 | '[0-9]+',
30 | 'alpha' => '[a-zA-Z]+',
31 | 'alphanumeric' => '[a-zA-Z0-9]+',
32 | ];
33 |
34 |
35 | public static function create(): self
36 | {
37 | return new self();
38 | }
39 |
40 | public static function getDelimiter(): string
41 | {
42 | return self::$delimiter;
43 | }
44 |
45 | /**
46 | * Set custom route prefix delimiter
47 | * @param string $delimiter
48 | * @return $this
49 | */
50 | public function prefixDelimiter(string $delimiter): self
51 | {
52 | self::$delimiter = $delimiter;
53 | return $this;
54 | }
55 |
56 | /**
57 | * Retrieve routes
58 | * @param TheRoute[] $routes
59 | * @param array[] $defaultData
60 | * @return array[]
61 | */
62 | public function get(array $routes, array $defaultData = []): array
63 | {
64 | $this->routeDefaultData = $defaultData;
65 |
66 | $routes = $this->loop($routes);
67 |
68 | $this->build($routes);
69 |
70 | return $this->routes;
71 | }
72 |
73 | /**
74 | * Loop through routes
75 | * @param TheRoute[] $routes
76 | * @return array
77 | */
78 | private function loop(array $routes): array
79 | {
80 | $results = [];
81 |
82 | for ($i = 0; $i < count($routes); $i++) {
83 | $route = $routes[$i]->getData();
84 | $results[$i]['route'] = $route;
85 | if (!empty($route['group'])) {
86 | $results[$i]['children'] = $this->loop($this->getGroup($route['group']));
87 | }
88 | }
89 |
90 | $this->routes = [];
91 |
92 | return $results;
93 | }
94 |
95 | /**
96 | * Get routes grouped together
97 | * @param callable $callback
98 | * @return TheRoute[]
99 | */
100 | private function getGroup(callable $callback): array
101 | {
102 | Route::restart();
103 | $callback();
104 | return Route::getRoutes();
105 | }
106 |
107 | /**
108 | * Build route structure
109 | * @param array $routes
110 | * @param array $parent
111 | */
112 | private function build(array $routes, array $parent = []): void
113 | {
114 | foreach ($routes as $route) {
115 | $routeData = $route['route'];
116 | if (isset($route['route'])) {
117 |
118 | if (isset($parent['middleware'])) {
119 | if (!isset($parent['middleware'])) {
120 | $routeData['middleware'][] = $parent['middleware'];
121 | }
122 | }
123 |
124 | $data = $this->constructRouteData($routeData, $parent);
125 |
126 | if (!empty($routeData['method'])) {
127 | $ready = $data;
128 | $prefix = $this->buildPrefix($ready['prepend'], $ready['prefix']);
129 | $prefix = $this->buildPrefix($prefix, $ready['append']);
130 | $ready['prefix'] = $prefix;
131 |
132 | //If default data is passed
133 | if (!empty($this->routeDefaultData)) {
134 | $ready['prefix'] = $this->buildPrefix(($this->routeDefaultData['prefix'] ?? ''), $ready['prefix']);
135 | $ready['namespace'] = ($this->routeDefaultData['namespace'] ?? '') . $ready['namespace'];
136 | $ready['name'] = ($this->routeDefaultData['name'] ?? '') . $ready['name'];
137 | if (isset($this->routeDefaultData['middleware'])) {
138 | $ready['middleware'][] = $this->routeDefaultData['middleware'];
139 | //$ready['middleware'] = $this->routeDefaultData['middleware'] . ($ready['middleware'] ? '|' . $ready['middleware'] : '');
140 | }
141 | } else {
142 | $ready['prefix'] = $this->removeRootDelimiter($ready['prefix']);
143 | }
144 |
145 | //We are now sure that all slashes at the prefix's beginning are cleaned
146 | //Now let's put it back
147 | $ready['prefix'] = self::$delimiter . $ready['prefix'];
148 |
149 | //Clean prefix
150 | $ready['prefix'] = $this->addRegExpToParams(
151 | $ready['prefix'],
152 | $ready['parameterTypes']
153 | );
154 |
155 | $this->routes[] = $ready;
156 | }
157 |
158 | if (isset($route['children'])) {
159 | $this->build($route['children'], $data);
160 | }
161 | }
162 | }
163 | }
164 |
165 | private function constructRouteData(array $routeData, array $parentData, bool $hasParentRoute = false): array
166 | {
167 | //Handle RouteInterface::match() routes
168 | if (isset($routeData['parentRoute'])) {
169 | $tempParentData = $routeData['parentRoute']->getData();
170 | $tempParentData['prefix'] = $routeData['prefix'];
171 | unset($routeData['parentRoute']);
172 | $routeData = $this->constructRouteData($routeData, $tempParentData, true);
173 | }
174 |
175 | if (isset($parentData['prefix']) && !$hasParentRoute) {
176 | $parentPrefix = $parentData['prefix'];
177 | if (!empty($routeData['prefix'])) {
178 | $parentPrefix = $parentPrefix . ($routeData['prefix'] == self::$delimiter ? '' : $routeData['prefix']);
179 | }
180 | }
181 |
182 | return [
183 | 'prefix' => $parentPrefix ?? $routeData['prefix'],
184 | 'append' => $this->buildPrefix(
185 | $this->getNullableString($routeData, 'append'),
186 | $this->getNullableString($parentData, 'append')
187 | ),
188 | 'prepend' => $this->buildPrefix(
189 | $this->getNullableString($parentData, 'prepend'),
190 | $this->getNullableString($routeData, 'prepend')
191 | ),
192 | 'namespace' => $this->getNullableString($parentData, 'namespace') . $routeData['namespace'],
193 | 'name' => $this->getNullableString($parentData, 'name') . $routeData['name'],
194 | 'handler' => $routeData['handler'],
195 | 'method' => $routeData['method'],
196 | 'middleware' => array_merge_recursive($parentData['middleware'] ?? [], $routeData['middleware']),
197 | 'fields' => array_merge($parentData['fields'] ?? [], $routeData['fields']),
198 | 'parameterTypes' => array_merge_recursive($parentData['parameterTypes'] ?? [], $routeData['parameterTypes']),
199 | ];
200 | }
201 |
202 | /**
203 | * Carefully join two prefix together
204 | *
205 | * @param string $prefix1
206 | * @param string $prefix2
207 | * @return string
208 | */
209 | private function buildPrefix(string $prefix1, string $prefix2): string
210 | {
211 | $prefix2 = $this->removeTrailingDelimiter($prefix2);
212 | if ($prefix2 && $prefix2 != self::$delimiter) {
213 | return ($prefix1 ? $prefix1 . self::$delimiter : '') . $prefix2;
214 | }
215 |
216 | return $prefix1;
217 | }
218 |
219 | /**
220 | * Remove slash at the end of prefix
221 | *
222 | * @param string $prefix
223 | * @return string
224 | */
225 | private function removeTrailingDelimiter(string $prefix): string
226 | {
227 | $prefixLength = strlen($prefix) - 1;
228 | if ($prefixLength > 0 && $prefix[$prefixLength] == self::$delimiter) {
229 | $prefix = $this->removeTrailingDelimiter(substr($prefix, 0, $prefixLength));
230 | }
231 |
232 | return $this->removeRootDelimiter($prefix);
233 | }
234 |
235 | /**
236 | * Remove slash at the beginning of prefix
237 | *
238 | * @param string $prefix
239 | * @return string
240 | */
241 | private function removeRootDelimiter(string $prefix): string
242 | {
243 | if (substr($prefix, 0, 1) == self::$delimiter) {
244 | return $this->removeRootDelimiter(substr($prefix, 1, strlen($prefix)));
245 | }
246 |
247 | return $prefix;
248 | }
249 |
250 | /**
251 | * Retrieve string from an array
252 | *
253 | * @param string[] $array
254 | * @param string $key
255 | * @return string
256 | */
257 | private function getNullableString(array $array, string $key): string
258 | {
259 | return $array[$key] ?? '';
260 | }
261 |
262 | /**
263 | * @param string $prefix
264 | * @param array $regExpGroups
265 | * @return string
266 | */
267 | private function addRegExpToParams(string $prefix, array $regExpGroups): string
268 | {
269 | foreach ($regExpGroups as $type => $regExpGroup) {
270 | foreach ($regExpGroup as $paramName => $regExp) {
271 | if ('regExp' == $type) {
272 | // Handle whereNumber(), whereAlpha(), whereAlphanumeric() data
273 | $paramType = '{' . "$paramName:$regExp" . '}';
274 | $prefix = str_replace('{' . $paramName . '}', $paramType, $prefix);
275 | } else {
276 | $paramType = '{' . "$regExp:{$this->defaultParameterTypes[$type]}" . '}';
277 | $prefix = str_replace('{' . $regExp . '}', $paramType, $prefix);
278 | }
279 | }
280 | }
281 |
282 | return $prefix;
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/Router/TheRoute.php:
--------------------------------------------------------------------------------
1 | [],
23 | 'alpha' => [],
24 | 'alphanumeric' => [],
25 | 'regExp' => [],
26 | ];
27 | protected Closure $group;
28 | protected TheRoute $parentRoute;
29 |
30 | /**
31 | * @var mixed Route handler/handler
32 | */
33 | protected $handler;
34 |
35 |
36 | public function __construct(?TheRoute $parentRoute = null)
37 | {
38 | if (null !== $parentRoute) {
39 | $this->parentRoute = $parentRoute;
40 | }
41 | }
42 |
43 | /**
44 | * @inheritDoc
45 | */
46 | public function head(string $route, $handler): RouteInterface
47 | {
48 | return $this->addRoute('HEAD', $route, $handler);
49 | }
50 |
51 | /**
52 | * Add route
53 | * @param string $method
54 | * @param string $route
55 | * @param mixed $handlerClass
56 | * @return $this
57 | */
58 | protected function addRoute(string $method, string $route, $handlerClass): RouteInterface
59 | {
60 | $this->method = $method;
61 | $this->prefix = $route;
62 | $this->handler = $handlerClass;
63 | return $this;
64 | }
65 |
66 | /**
67 | * @inheritDoc
68 | */
69 | public function match(array $methods, string $uri, $handler): RouteInterface
70 | {
71 | foreach ($methods as $method) {
72 | $method = strtolower($method);
73 | $route = new TheRoute($this);
74 | $route->name(strtolower($method));
75 | Route::push($route);
76 | $route->$method($uri, $handler);
77 | }
78 |
79 | return $this;
80 | }
81 |
82 | /**
83 | * @inheritDoc
84 | */
85 | public function any(array $paths, string $method, $handler): RouteInterface
86 | {
87 | foreach ($paths as $path) {
88 | $method = strtolower($method);
89 | $route = new TheRoute($this);
90 | Route::push($route);
91 | $route->$method($path, $handler);
92 | }
93 |
94 | return $this;
95 | }
96 |
97 | /**
98 | * @inheritDoc
99 | */
100 | public function matchAny(array $methods, array $paths, $handler): RouteInterface
101 | {
102 | foreach ($methods as $method) {
103 | foreach ($paths as $path) {
104 | $method = strtolower($method);
105 | $route = new TheRoute($this);
106 | Route::push($route);
107 | $route->$method($path, $handler);
108 | }
109 | }
110 |
111 | return $this;
112 | }
113 |
114 | /**
115 | * @inheritDoc
116 | */
117 | public function resource(
118 | string $uri,
119 | string $controller,
120 | string $idParameterName = 'id',
121 | bool $integerId = true
122 | ): RouteInterface
123 | {
124 | $idParam = $integerId
125 | ? '{' . "$idParameterName:[0-9]+" . '}'
126 | : '{' . "$idParameterName" . '}';
127 |
128 | // GET /whatever
129 | $route = new TheRoute($this);
130 | $route->get($uri, [$controller, 'index'])->name('index');
131 | Route::push($route);
132 |
133 | // GET /whatever/create
134 | $route = new TheRoute($this);
135 | $route->get("$uri/create", [$controller, 'create'])->name('create');
136 | Route::push($route);
137 |
138 | // POST /whatever
139 | $route = new TheRoute($this);
140 | $route->post($uri, [$controller, 'store'])->name('store');
141 | Route::push($route);
142 |
143 | // GET /whatever/{$id}
144 | $route = new TheRoute($this);
145 | $route->get("$uri/$idParam", [$controller, 'show'])->name('show');
146 | Route::push($route);
147 |
148 | // GET /whatever/{$id}/edit
149 | $route = new TheRoute($this);
150 | $route->get("$uri/$idParam/edit", [$controller, 'edit'])->name('edit');
151 | Route::push($route);
152 |
153 | // PUT /whatever/{$id}
154 | $route = new TheRoute($this);
155 | $route->put("$uri/$idParam", [$controller, 'update'])->name('update');
156 | Route::push($route);
157 |
158 | // PATCH /whatever/{$id}
159 | $route = new TheRoute($this);
160 | $route->patch("$uri/$idParam", [$controller, 'update'])->name('update');
161 | Route::push($route);
162 |
163 | // DELETE /whatever/{$id}
164 | $route = new TheRoute($this);
165 | $route->delete("$uri/$idParam", [$controller, 'destroy'])->name('destroy');
166 | Route::push($route);
167 |
168 | return $this;
169 | }
170 |
171 | /**
172 | * @inheritDoc
173 | */
174 | public function get(string $route, $handler): RouteInterface
175 | {
176 | return $this->addRoute('GET', $route, $handler);
177 | }
178 |
179 | /**
180 | * @inheritDoc
181 | */
182 | public function post(string $route, $handler): RouteInterface
183 | {
184 | return $this->addRoute('POST', $route, $handler);
185 | }
186 |
187 | /**
188 | * @inheritDoc
189 | */
190 | public function put(string $route, $handler): RouteInterface
191 | {
192 | return $this->addRoute('PUT', $route, $handler);
193 | }
194 |
195 | /**
196 | * @inheritDoc
197 | */
198 | public function patch(string $route, $handler): RouteInterface
199 | {
200 | return $this->addRoute('PATCH', $route, $handler);
201 | }
202 |
203 | /**
204 | * @inheritDoc
205 | */
206 | public function delete(string $route, $handler): RouteInterface
207 | {
208 | return $this->addRoute('DELETE', $route, $handler);
209 | }
210 |
211 | /**
212 | * @inheritDoc
213 | */
214 | public function prefix(string $prefix): RouteInterface
215 | {
216 | $this->prefix = $prefix;
217 | return $this;
218 | }
219 |
220 | /**
221 | * @inheritDoc
222 | */
223 | public function append(string $prefix): RouteInterface
224 | {
225 | $this->append = $prefix;
226 | return $this;
227 | }
228 |
229 | /**
230 | * @inheritDoc
231 | */
232 | public function prepend(string $prefix): RouteInterface
233 | {
234 | $this->prepend = $prefix;
235 | return $this;
236 | }
237 |
238 | /**
239 | * @inheritDoc
240 | */
241 | public function group(Closure $closure): RouteInterface
242 | {
243 | $this->group = $closure;
244 | return $this;
245 | }
246 |
247 | /**
248 | * @inheritDoc
249 | */
250 | public function namespace(string $namespace): RouteInterface
251 | {
252 | if ($namespace[strlen($namespace) - 1] !== "\\") {
253 | $namespace .= "\\";
254 | }
255 | $this->namespace = $namespace;
256 | return $this;
257 | }
258 |
259 | /**
260 | * @inheritDoc
261 | */
262 | public function name(string $name): RouteInterface
263 | {
264 | $this->name = $name;
265 | return $this;
266 | }
267 |
268 | /**
269 | * @inheritDoc
270 | */
271 | public function middleware($middleware): RouteInterface
272 | {
273 | if (is_array($middleware)) {
274 | $this->middlewares = array_merge($this->middlewares, $middleware);
275 | return $this;
276 | }
277 |
278 | $this->middlewares[] = $middleware;
279 | return $this;
280 | }
281 |
282 | /**
283 | * @inheritDoc
284 | */
285 | public function addField(string $name, $value): RouteInterface
286 | {
287 | $this->fields[$name] = $value;
288 | return $this;
289 | }
290 |
291 | /**
292 | * @inheritDoc
293 | */
294 | public function where($parameter, ?string $regExp = null): RouteInterface
295 | {
296 | if (is_array($parameter)) {
297 | $this->parameterTypes['regExp'] = array_merge($this->parameterTypes['regExp'], $parameter);
298 | } else {
299 | if (null === $regExp) {
300 | throw new ValueError('Second parameter must not be null when string is passed to first parameter.');
301 | }
302 | $this->parameterTypes['regExp'] += [$parameter => $regExp];
303 | }
304 |
305 | return $this;
306 | }
307 |
308 | /**
309 | * @inheritDoc
310 | */
311 | public function whereNumber(string $param): RouteInterface
312 | {
313 | $this->parameterTypes['number'][] = $param;
314 | return $this;
315 | }
316 |
317 | /**
318 | * @inheritDoc
319 | */
320 | public function whereAlpha(string $param): RouteInterface
321 | {
322 | $this->parameterTypes['alpha'][] = $param;
323 | return $this;
324 | }
325 |
326 | /**
327 | * @inheritDoc
328 | */
329 | public function whereAlphaNumeric(string $param): RouteInterface
330 | {
331 | $this->parameterTypes['alphanumeric'][] = $param;
332 | return $this;
333 | }
334 |
335 | /**
336 | * @inheritDoc
337 | */
338 | public function jsonSerialize(): array
339 | {
340 | return $this->getData();
341 | }
342 |
343 | public function getData(): array
344 | {
345 | $this->onRegister();
346 |
347 | $routeData = [
348 | 'prefix' => $this->prefix,
349 | 'namespace' => $this->namespace,
350 | 'handler' => $this->handler,
351 | 'middleware' => $this->middlewares,
352 | 'method' => $this->method,
353 | 'name' => $this->name,
354 | 'prepend' => $this->prepend,
355 | 'append' => $this->append,
356 | 'group' => $this->group ?? null,
357 | 'fields' => $this->fields,
358 | 'parameterTypes' => $this->parameterTypes,
359 | ];
360 |
361 | if (isset($this->parentRoute)) {
362 | $routeData['parentRoute'] = $this->parentRoute;
363 | }
364 |
365 | return $routeData;
366 | }
367 |
368 | /**
369 | * @inheritDoc
370 | */
371 | public function onRegister(): RouteInterface
372 | {
373 | if (substr($this->prefix, 0, 1) != Getter::getDelimiter()) {
374 | $this->prefix = Getter::getDelimiter() . $this->prefix;
375 | }
376 |
377 | return $this;
378 | }
379 |
380 | /**
381 | * @inheritDoc
382 | */
383 | public function getRouteData(): array
384 | {
385 | return $this->getData();
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/tests/RouteRegisterTest.php:
--------------------------------------------------------------------------------
1 | createTheRoute();
18 | $theRoute->prefix('hello');
19 | $theRoute->name('name');
20 | $theRoute->middleware('middleware');
21 | $theRoute->namespace('Name\Space');
22 | $theRoute->addField('test', 'success');
23 |
24 | $routeData = $theRoute->getData();
25 | $this->assertEquals('hello', $routeData['prefix']);
26 | $this->assertEquals('name', $routeData['name']);
27 | $this->assertEquals(['middleware'], $routeData['middleware']);
28 | $this->assertEquals('Name\Space\\', $routeData['namespace']);
29 | }
30 |
31 | protected function createTheRoute(string $delimiter = '/'): TheRouteFactory
32 | {
33 | Getter::create()->prefixDelimiter($delimiter);
34 | return (new TheRouteFactory());
35 | }
36 |
37 | public function testAppend(): void
38 | {
39 | Route::append('earth')->group(function () {
40 | Route::get('planets', fn() => print time());
41 | });
42 |
43 | $routeData = Getter::create()->get(Route::getRoutes())[0];
44 | $this->assertEquals('/planets/earth', $routeData['prefix']);
45 | }
46 |
47 | public function testPrepend(): void
48 | {
49 | Route::restart();
50 | Route::prepend('galaxies')->group(function () {
51 | Route::get('milkyway', fn() => print time());
52 | });
53 |
54 | $routeData = Getter::create()->get(Route::getRoutes())[0];
55 | $this->assertEquals('/galaxies/milkyway', $routeData['prefix']);
56 | }
57 |
58 | public function testGroup(): void
59 | {
60 | Route::restart();
61 | Route::prefix('one')->group(function () {
62 | Route::get('route', fn() => time());
63 | });
64 |
65 | Route::prefix('start')
66 | ->append('end')
67 | ->group(function () {
68 | Route::get('middle', fn() => time());
69 | Route::prefix('inner')->group(function () {
70 | Route::get('route', fn() => printer());
71 | });
72 | });
73 |
74 | Route::prefix('middle')
75 | ->prepend('start')
76 | ->group(function () {
77 | Route::get('end', fn() => time());
78 | });
79 |
80 | Route::name('planets.')
81 | ->group(function () {
82 | Route::get('earth', fn() => time())->name('earth');
83 | });
84 |
85 | $routeData = $this->getRouteData();
86 | $this->assertEquals('/one/route', $routeData[0]['prefix']);
87 | $this->assertEquals('/start/middle/end', $routeData[1]['prefix']);
88 | $this->assertEquals('/start/inner/route/end', $routeData[2]['prefix']);
89 | $this->assertEquals('/start/middle/end', $routeData[3]['prefix']);
90 | $this->assertEquals('planets.earth', $routeData[4]['name']);
91 | }
92 |
93 | protected function getRouteData(): array
94 | {
95 | return Getter::create()->get(Route::getRoutes());
96 | }
97 |
98 | public function testRequestMethods(): void
99 | {
100 | $theRoute = $this->createTheRoute();
101 | //GET method
102 | $theRoute->get('user', fn() => print time());
103 | $routeData = $theRoute->getData();
104 | $this->assertEquals('GET', $routeData['method']);
105 | $this->assertEquals('user', $routeData['prefix']);
106 |
107 | //POST method
108 | $theRoute->post('create', fn() => print time());
109 | $routeData = $theRoute->getData();
110 | $this->assertEquals('POST', $routeData['method']);
111 | $this->assertEquals('create', $routeData['prefix']);
112 |
113 | //DELETE method
114 | $theRoute->delete('1', fn() => print time());
115 | $routeData = $theRoute->getData();
116 | $this->assertEquals('DELETE', $routeData['method']);
117 | $this->assertEquals('1', $routeData['prefix']);
118 |
119 | //PUT method
120 | $theRoute->put('2', fn() => print time());
121 | $routeData = $theRoute->getData();
122 | $this->assertEquals('PUT', $routeData['method']);
123 | $this->assertEquals('2', $routeData['prefix']);
124 |
125 | //PATCH method
126 | $theRoute->patch('3', fn() => print time());
127 | $routeData = $theRoute->getData();
128 | $this->assertEquals('PATCH', $routeData['method']);
129 | $this->assertEquals('3', $routeData['prefix']);
130 |
131 | //PATCH method
132 | $theRoute->head('test', fn() => print time());
133 | $routeData = $theRoute->getData();
134 | $this->assertEquals('HEAD', $routeData['method']);
135 | $this->assertEquals('test', $routeData['prefix']);
136 | }
137 |
138 | public function testMatch(): void
139 | {
140 | $fn = fn() => print time();
141 | Route::restart();
142 | Route::match(['GET', 'POST'], 'login', $fn)
143 | ->namespace('Auth');
144 |
145 | Route::match(['DELETE', 'GET'], 'user', $fn)
146 | ->middleware('auth')
147 | ->addField('test', 'field');
148 |
149 | $collector = Collector::create()->collect();
150 |
151 | $dispatchResult1 = Dispatcher::create($collector)
152 | ->dispatch('get', '/login');
153 |
154 | $dispatchResult2 = Dispatcher::create($collector)
155 | ->dispatch('delete', '/user');
156 |
157 | self::assertSame([], $dispatchResult1->getRoute()->getMiddleware());
158 | self::assertSame('Auth\\', $dispatchResult1->getRoute()->getNamespace());
159 | self::assertSame(['auth'], $dispatchResult2->getRoute()->getMiddleware());
160 | self::assertSame([
161 | 'test' => 'field'
162 | ], $dispatchResult2->getRoute()->getFields());
163 | }
164 |
165 | public function testMatchWithNamedRoutes(): void
166 | {
167 | Route::restart();
168 |
169 | Route::match(['get', 'post'], 'login', ['showLoginForm'])->name('login.');
170 | Route::match(['get', 'post'], 'register', ['showRegisterForm'])->name('register.');
171 |
172 | $routes = Collector::create()
173 | ->collect()
174 | ->getCollectedRoutes();
175 |
176 | self::assertSame('login.get', $routes[0]['name']);
177 | self::assertSame('login.post', $routes[1]['name']);
178 | self::assertSame('register.get', $routes[2]['name']);
179 | self::assertSame('register.post', $routes[3]['name']);
180 | }
181 |
182 | public function testAny(): void
183 | {
184 | $handler = 'strtoupper';
185 | Route::any([
186 | '/',
187 | '/login',
188 | '/admin/login'
189 | ], 'get', $handler);
190 |
191 | Route::create()
192 | ->prepend('server')
193 | ->any(
194 | ['home', 'panel'],
195 | 'post',
196 | $handler
197 | );
198 |
199 |
200 | $collector = Collector::create()->collect();
201 | $dispatchResult1 = Dispatcher::create($collector)
202 | ->dispatch('get', '/');
203 | $dispatchResult2 = Dispatcher::create($collector)
204 | ->dispatch('get', '/login');
205 | $dispatchResult3 = Dispatcher::create($collector)
206 | ->dispatch('get', '/admin/login');
207 | $dispatchResult4 = Dispatcher::create($collector)
208 | ->dispatch('post', '/server/home');
209 | $dispatchResult5 = Dispatcher::create($collector)
210 | ->dispatch('post', '/server/panel');
211 |
212 | self::assertSame('/', $dispatchResult1->getRoute()->getPrefix());
213 | self::assertSame('/login', $dispatchResult2->getRoute()->getPrefix());
214 | self::assertSame('/admin/login', $dispatchResult3->getRoute()->getPrefix());
215 | self::assertSame('/admin/login', $dispatchResult3->getRoute()->getPrefix());
216 | self::assertSame('/server/home', $dispatchResult4->getRoute()->getPrefix());
217 | self::assertSame('/server/panel', $dispatchResult5->getRoute()->getPrefix());
218 | }
219 |
220 | public function testMiddleware(): void
221 | {
222 | Route::restart();
223 | Route::get('/', fn() => print time())->middleware('only');
224 | Route::prefix('/admin')
225 | ->middleware('admin')
226 | ->group(function () {
227 | Route::prefix('user')->group(function () {
228 | Route::get('/', fn() => print time())->middleware('user');
229 | });
230 | });
231 |
232 | $collector = Collector::create()->collect();
233 | $result1 = Dispatcher::create($collector)->dispatch('get', '/');
234 | $result2 = Dispatcher::create($collector)->dispatch('get', '/admin/user');
235 | self::assertSame(['only'], $result1->getRoute()->getMiddleware());
236 | self::assertSame(['admin', 'user'], $result2->getRoute()->getMiddleware());
237 | }
238 |
239 | public function testArrayOfMiddlewares(): void
240 | {
241 | Route::restart();
242 | Route::get('/', fn() => print time())->middleware('only');
243 | Route::prefix('/admin')
244 | ->middleware(['admin', 'super'])
245 | ->group(function () {
246 | Route::prefix('user')->group(function () {
247 | Route::get('/', fn() => print time())->middleware(['user', 'anonymous']);
248 | });
249 | });
250 |
251 | $collector = Collector::create()->collect();
252 | $result1 = Dispatcher::create($collector)->dispatch('get', '/');
253 | $result2 = Dispatcher::create($collector)->dispatch('get', '/admin/user');
254 | self::assertSame(['only'], $result1->getRoute()->getMiddleware());
255 | self::assertSame(['admin', 'super', 'user', 'anonymous'], $result2->getRoute()->getMiddleware());
256 | }
257 |
258 | public function testResource(): void
259 | {
260 | $this->resourceTester();
261 | }
262 |
263 | public function resourceTester(string $idParamName = 'id', bool $integerParam = true): void
264 | {
265 | $idParam = '';
266 | Route::restart();
267 | Route::prefix('user')
268 | ->name('user.')
269 | ->group(function () use ($idParamName, $integerParam, &$idParam) {
270 | $idParam = $integerParam ? '{' . $idParamName . ':[0-9]+}' : '{' . $idParamName . '}';
271 | Route::resource('photos', 'App\Http\Controller\PhotoController', $idParamName, $integerParam);
272 | });
273 |
274 |
275 | $routes = Collector::create()
276 | ->collect()
277 | ->getCollectedRoutes();
278 |
279 | self::assertSame('/user/photos', $routes[0]['prefix']);
280 | self::assertSame('GET', $routes[0]['method']);
281 |
282 | self::assertSame('/user/photos/create', $routes[1]['prefix']);
283 | self::assertSame('GET', $routes[1]['method']);
284 |
285 | self::assertSame('/user/photos', $routes[2]['prefix']);
286 | self::assertSame('POST', $routes[2]['method']);
287 |
288 | self::assertSame('/user/photos/' . $idParam, $routes[3]['prefix']);
289 | self::assertSame('GET', $routes[3]['method']);
290 |
291 | self::assertSame('/user/photos/' . $idParam . '/edit', $routes[4]['prefix']);
292 | self::assertSame('GET', $routes[4]['method']);
293 |
294 | self::assertSame('/user/photos/' . $idParam, $routes[5]['prefix']);
295 | self::assertSame('PUT', $routes[5]['method']);
296 |
297 | self::assertSame('/user/photos/' . $idParam, $routes[6]['prefix']);
298 | self::assertSame('PATCH', $routes[6]['method']);
299 |
300 | self::assertSame('/user/photos/' . $idParam, $routes[7]['prefix']);
301 | self::assertSame('DELETE', $routes[7]['method']);
302 | }
303 |
304 | public function testResourceWithCustomIdParamName(): void
305 | {
306 | $this->resourceTester('userId');
307 | }
308 |
309 | public function testResourceWithNonIntegerParam(): void
310 | {
311 | $this->resourceTester('userId', false);
312 | }
313 |
314 | public function testMatchAny(): void
315 | {
316 | Route::restart();
317 | Route::matchAny(['get', 'post'], ['/customer/login', '/admin/login'], 'MainController@index');
318 | $routes = Collector::create()->collect()->getCollectedRoutes();
319 |
320 | self::assertSame('GET', $routes[0]['method']);
321 | self::assertSame('/customer/login', $routes[0]['prefix']);
322 | self::assertSame('GET', $routes[1]['method']);
323 | self::assertSame('/admin/login', $routes[1]['prefix']);
324 | self::assertSame('POST', $routes[2]['method']);
325 | self::assertSame('/customer/login', $routes[2]['prefix']);
326 | self::assertSame('POST', $routes[3]['method']);
327 | self::assertSame('/admin/login', $routes[3]['prefix']);
328 | }
329 |
330 | public function testControllerMethod(): void
331 | {
332 | Route::get('/', [1, 3]);
333 | $result = Dispatcher::collectRoutes()->dispatch('GET', '/');
334 | self::assertSame([1, 3], $result->getRoute()->getController());
335 | }
336 |
337 | protected function setUp(): void
338 | {
339 | Route::restart();
340 | }
341 | }
--------------------------------------------------------------------------------