├── 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 | ![alt text](resource-http-verbs.png) 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 | ![alt text](crud-definition.png) 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 | } --------------------------------------------------------------------------------