From 362f3a049c8b209a38bac95fde22417084359846 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Wed, 3 Jan 2024 13:08:39 -0700 Subject: [PATCH 1/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db211b3..79b02f2 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If you're using [Composer](https://getcomposer.org/), you can run the following command: ```bash -composer require n0nag0n/flight +composer require flightphp/core ``` OR you can [download](https://github.com/n0nag0n/flight/archive/master.zip) From 0b149349fcead1909ea8d03c85670ef9d089203a Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Wed, 3 Jan 2024 14:01:12 -0700 Subject: [PATCH 2/5] Test to confirm #161 is fixed. --- tests/RouterTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 9adc7fc..6715652 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -208,6 +208,12 @@ class RouterTest extends PHPUnit\Framework\TestCase $this->check('OK'); } + public function testWildcardDuplicate() { + $this->router->map('/account/*' , [$this, 'ok']); + $this->request->url = '/account/account/account'; + $this->check('OK'); + } + // Check if route object was passed public function testRouteObjectPassing() { From 12073629cc2089c7e4de13a65cb56c10b998e783 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Wed, 3 Jan 2024 17:16:56 -0700 Subject: [PATCH 3/5] added ability to group routes together --- flight/Engine.php | 14 +++++++++++++- flight/Flight.php | 1 + flight/net/Router.php | 23 +++++++++++++++++++++- tests/FlightTest.php | 22 +++++++++++++++++++++ tests/RouterTest.php | 45 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 4b1bdca..47866ca 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -33,6 +33,7 @@ use Throwable; * * Routing * @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function. + * @method void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. * @method void get(string $pattern, callable $callback, bool $pass_route = false) Routes a GET URL to a callback function. * @method void post(string $pattern, callable $callback, bool $pass_route = false) Routes a POST URL to a callback function. * @method void put(string $pattern, callable $callback, bool $pass_route = false) Routes a PUT URL to a callback function. @@ -151,7 +152,7 @@ class Engine $methods = [ 'start', 'stop', 'route', 'halt', 'error', 'notFound', 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', - 'post', 'put', 'patch', 'delete', + 'post', 'put', 'patch', 'delete', 'group', ]; foreach ($methods as $name) { $this->dispatcher->set($name, [$this, '_' . $name]); @@ -468,6 +469,17 @@ class Engine $this->router()->map($pattern, $callback, $pass_route); } + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function that includes the Router class as first parameter + */ + public function _group(string $pattern, callable $callback): void + { + $this->router()->group($pattern, $callback); + } + /** * Routes a URL to a callback function. * diff --git a/flight/Flight.php b/flight/Flight.php index 69a8b65..7dac34d 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -24,6 +24,7 @@ use flight\template\View; * @method static void halt(int $code = 200, string $message = '') Stop the framework with an optional status code and message. * * @method static void route(string $pattern, callable $callback, bool $pass_route = false) Maps a URL pattern to a callback. + * @method static void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. * @method static Router router() Returns Router instance. * * @method static void map(string $name, callable $callback) Creates a custom framework method. diff --git a/flight/net/Router.php b/flight/net/Router.php index f1f8842..d08e039 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -32,6 +32,13 @@ class Router */ protected int $index = 0; + /** + * When groups are used, this is mapped against all the routes + * + * @var string + */ + protected string $group_prefix = ''; + /** * Gets mapped routes. * @@ -59,7 +66,7 @@ class Router */ public function map(string $pattern, callable $callback, bool $pass_route = false): void { - $url = trim($pattern); + $url = $this->group_prefix.trim($pattern); $methods = ['*']; if (false !== strpos($url, ' ')) { @@ -71,6 +78,20 @@ class Router $this->routes[] = new Route($url, $callback, $methods, $pass_route); } + /** + * Group together a set of routes + * + * @param string $group_prefix group URL prefix (such as /api/v1) + * @param callable $callback The necessary calling that holds the Router class + * @return void + */ + public function group(string $group_prefix, callable $callback): void { + $old_group_prefix = $this->group_prefix; + $this->group_prefix .= $group_prefix; + $callback($this); + $this->group_prefix = $old_group_prefix; + } + /** * Routes the current request. * diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 588ebb2..8254e60 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -96,4 +96,26 @@ class FlightTest extends PHPUnit\Framework\TestCase Flight::doesNotExist(); } + + public function testStaticRoute() { + Flight::route('/test', function() { + echo 'test'; + }); + Flight::request()->url = '/test'; + Flight::start(); + + $this->expectOutputString('test'); + } + + public function testStaticRouteGroup() { + Flight::group('/group', function() { + Flight::route('/test', function() { + echo 'test'; + }); + }); + Flight::request()->url = '/group/test'; + Flight::start(); + + $this->expectOutputString('test'); + } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 6715652..45b1f7c 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -358,4 +358,49 @@ class RouterTest extends PHPUnit\Framework\TestCase $router->reset(); $this->assertEquals(0, $router->getIndex()); } + + // Passing URL parameters + public function testGroupRoutes() + { + $this->router->group('/user', function(Router $router) { + $router->map('/@id', function ($id) { + echo $id; + }); + $router->map('/@id/@name', function ($id, $name) { + echo $id . $name; + }); + }); + $this->request->url = '/user/123'; + $this->check('123'); + } + + public function testGroupRoutesMultiParams() + { + $this->router->group('/user', function(Router $router) { + $router->map('/@id', function ($id) { + echo $id; + }); + $router->map('/@id/@name', function ($id, $name) { + echo $id . $name; + }); + }); + $this->request->url = '/user/123/abc'; + $this->check('123abc'); + } + + public function testGroupNestedRoutes() + { + $this->router->group('/client', function(Router $router) { + $router->group('/user', function(Router $router) { + $router->map('/@id', function ($id) { + echo $id; + }); + $router->map('/@id/@name', function ($id, $name) { + echo $id . $name; + }); + }); + }); + $this->request->url = '/client/user/123/abc'; + $this->check('123abc'); + } } From a3555b019fa306be59b55006e1d4da44dde261bb Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Thu, 4 Jan 2024 10:02:33 -0700 Subject: [PATCH 4/5] Added documentation to readme on grouping --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ tests/FlightTest.php | 6 +++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79b02f2..c8f25c0 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,61 @@ Flight::route('/', function(\flight\net\Route $route) { }, true); ``` +## Route Grouping + +There may be times when you want to group related routes together (such as `/api/v1`). +You can do this by using the `group` method: + +```php +Flight::group('/api/v1', function () { + Flight::route('/users', function () { + // Matches /api/v1/users + }); + + Flight::route('/posts', function () { + // Matches /api/v1/posts + }); +}); +``` + +You can even nest groups of groups: + +```php +Flight::group('/api', function () { + Flight::group('/v1', function () { + Flight::route('/users', function () { + // Matches /api/v1/users + }); + + Flight::route('/posts', function () { + // Matches /api/v1/posts + }); + }); + Flight::group('/v2', function () { + Flight::route('/users', function () { + // Matches /api/v2/users + }); + }); +}); +``` + +### Grouping with Object Context + +You can still use route grouping with the `Engine` object in the following way: + +```php +$app = new \flight\Engine(); +$app->group('/api/v1', function (Router $router) { + $router->map('/users', function () { + // Matches /api/v1/users + }); + + $router->map('/posts', function () { + // Matches /api/v1/posts + }); +}); +``` + # Extending Flight is designed to be an extensible framework. The framework comes with a set diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 8254e60..504ebc1 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -102,9 +102,10 @@ class FlightTest extends PHPUnit\Framework\TestCase echo 'test'; }); Flight::request()->url = '/test'; + + $this->expectOutputString('test'); Flight::start(); - $this->expectOutputString('test'); } public function testStaticRouteGroup() { @@ -114,8 +115,9 @@ class FlightTest extends PHPUnit\Framework\TestCase }); }); Flight::request()->url = '/group/test'; - Flight::start(); $this->expectOutputString('test'); + Flight::start(); + } } From 65b3d5445fcafb9246ed46ba818c7c373c9dd580 Mon Sep 17 00:00:00 2001 From: Austin Collier Date: Thu, 4 Jan 2024 20:21:01 -0700 Subject: [PATCH 5/5] Lots more unit testing for group based routing --- README.md | 27 ++++++++++------ flight/Engine.php | 1 - flight/Flight.php | 17 ++++++++++- flight/net/Router.php | 65 +++++++++++++++++++++++++++++++++++++-- tests/FlightTest.php | 71 +++++++++++++++++++++++++++++++++++++++++-- tests/RouterTest.php | 60 +++++++++++++++++++++++++++++++++++- 6 files changed, 224 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c8f25c0..f2856ff 100644 --- a/README.md +++ b/README.md @@ -313,17 +313,24 @@ You can even nest groups of groups: ```php Flight::group('/api', function () { Flight::group('/v1', function () { - Flight::route('/users', function () { - // Matches /api/v1/users + // Flight::get() gets variables, it doesn't set a route! See object context below + Flight::route('GET /users', function () { + // Matches GET /api/v1/users }); - Flight::route('/posts', function () { - // Matches /api/v1/posts + Flight::post('/posts', function () { + // Matches POST /api/v1/posts + }); + + Flight::put('/posts/1', function () { + // Matches PUT /api/v1/posts }); }); Flight::group('/v2', function () { - Flight::route('/users', function () { - // Matches /api/v2/users + + // Flight::get() gets variables, it doesn't set a route! See object context below + Flight::route('GET /users', function () { + // Matches GET /api/v2/users }); }); }); @@ -336,12 +343,12 @@ You can still use route grouping with the `Engine` object in the following way: ```php $app = new \flight\Engine(); $app->group('/api/v1', function (Router $router) { - $router->map('/users', function () { - // Matches /api/v1/users + $router->get('/users', function () { + // Matches GET /api/v1/users }); - $router->map('/posts', function () { - // Matches /api/v1/posts + $router->post('/posts', function () { + // Matches POST /api/v1/posts }); }); ``` diff --git a/flight/Engine.php b/flight/Engine.php index 47866ca..2628a11 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -34,7 +34,6 @@ use Throwable; * Routing * @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function. * @method void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. - * @method void get(string $pattern, callable $callback, bool $pass_route = false) Routes a GET URL to a callback function. * @method void post(string $pattern, callable $callback, bool $pass_route = false) Routes a POST URL to a callback function. * @method void put(string $pattern, callable $callback, bool $pass_route = false) Routes a PUT URL to a callback function. * @method void patch(string $pattern, callable $callback, bool $pass_route = false) Routes a PATCH URL to a callback function. diff --git a/flight/Flight.php b/flight/Flight.php index 7dac34d..b1a9b1f 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -25,6 +25,10 @@ use flight\template\View; * * @method static void route(string $pattern, callable $callback, bool $pass_route = false) Maps a URL pattern to a callback. * @method static void group(string $pattern, callable $callback) Groups a set of routes together under a common prefix. + * @method void post(string $pattern, callable $callback, bool $pass_route = false) Routes a POST URL to a callback function. + * @method void put(string $pattern, callable $callback, bool $pass_route = false) Routes a PUT URL to a callback function. + * @method void patch(string $pattern, callable $callback, bool $pass_route = false) Routes a PATCH URL to a callback function. + * @method void delete(string $pattern, callable $callback, bool $pass_route = false) Routes a DELETE URL to a callback function. * @method static Router router() Returns Router instance. * * @method static void map(string $name, callable $callback) Creates a custom framework method. @@ -124,11 +128,22 @@ class Flight if (!$initialized) { require_once __DIR__ . '/autoload.php'; - self::$engine = new Engine(); + self::setEngine(new Engine()); $initialized = true; } return self::$engine; } + + /** + * Set the engine instance + * + * @param Engine $engine Vroom vroom! + * @return void + */ + public static function setEngine(Engine $engine): void + { + self::$engine = $engine; + } } diff --git a/flight/net/Router.php b/flight/net/Router.php index d08e039..078948c 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -63,10 +63,11 @@ class Router * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback + * @return void */ public function map(string $pattern, callable $callback, bool $pass_route = false): void { - $url = $this->group_prefix.trim($pattern); + $url = trim($pattern); $methods = ['*']; if (false !== strpos($url, ' ')) { @@ -75,9 +76,69 @@ class Router $methods = explode('|', $method); } - $this->routes[] = new Route($url, $callback, $methods, $pass_route); + $this->routes[] = new Route($this->group_prefix.$url, $callback, $methods, $pass_route); } + /** + * Creates a GET based route + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @return void + */ + public function get(string $pattern, callable $callback, bool $pass_route = false): void { + $this->map('GET ' . $pattern, $callback, $pass_route); + } + + /** + * Creates a POST based route + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @return void + */ + public function post(string $pattern, callable $callback, bool $pass_route = false): void { + $this->map('POST ' . $pattern, $callback, $pass_route); + } + + /** + * Creates a PUT based route + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @return void + */ + public function put(string $pattern, callable $callback, bool $pass_route = false): void { + $this->map('PUT ' . $pattern, $callback, $pass_route); + } + + /** + * Creates a PATCH based route + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @return void + */ + public function patch(string $pattern, callable $callback, bool $pass_route = false): void { + $this->map('PATCH ' . $pattern, $callback, $pass_route); + } + + /** + * Creates a DELETE based route + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @return void + */ + public function delete(string $pattern, callable $callback, bool $pass_route = false): void { + $this->map('DELETE ' . $pattern, $callback, $pass_route); + } + /** * Group together a set of routes * diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 504ebc1..12b2772 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -1,5 +1,6 @@ url = '/test'; - + $this->expectOutputString('test'); Flight::start(); - } public function testStaticRouteGroup() { @@ -118,6 +120,71 @@ class FlightTest extends PHPUnit\Framework\TestCase $this->expectOutputString('test'); Flight::start(); + } + + public function testStaticRouteGet() { + + // can't actually get "get" because that gets a variable + Flight::route('GET /test', function() { + echo 'test get'; + }); + + $_SERVER['REQUEST_METHOD'] = 'GET'; + Flight::request()->url = '/test'; + + $this->expectOutputString('test get'); + Flight::start(); + } + + public function testStaticRoutePost() { + + Flight::post('/test', function() { + echo 'test post'; + }); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + Flight::request()->url = '/test'; + + $this->expectOutputString('test post'); + Flight::start(); + } + + public function testStaticRoutePut() { + + Flight::put('/test', function() { + echo 'test put'; + }); + + $_SERVER['REQUEST_METHOD'] = 'PUT'; + Flight::request()->url = '/test'; + + $this->expectOutputString('test put'); + Flight::start(); + } + public function testStaticRoutePatch() { + + Flight::patch('/test', function() { + echo 'test patch'; + }); + + $_SERVER['REQUEST_METHOD'] = 'PATCH'; + Flight::request()->url = '/test'; + + $this->expectOutputString('test patch'); + Flight::start(); + } + + public function testStaticRouteDelete() { + + Flight::delete('/test', function() { + echo 'test delete'; + }); + + $_SERVER['REQUEST_METHOD'] = 'DELETE'; + Flight::request()->url = '/test'; + + $this->expectOutputString('test delete'); + Flight::start(); } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 45b1f7c..5cc222d 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -106,7 +106,6 @@ class RouterTest extends PHPUnit\Framework\TestCase } // Simple path with trailing slash - // Simple path public function testPathRouteTrailingSlash() { $this->router->map('/path/', [$this, 'ok']); @@ -115,6 +114,15 @@ class RouterTest extends PHPUnit\Framework\TestCase $this->check('OK'); } + public function testGetRouteShortcut() + { + $this->router->get('/path', [$this, 'ok']); + $this->request->url = '/path'; + $this->request->method = 'GET'; + + $this->check('OK'); + } + // POST route public function testPostRoute() { @@ -125,6 +133,15 @@ class RouterTest extends PHPUnit\Framework\TestCase $this->check('OK'); } + public function testPostRouteShortcut() + { + $this->router->post('/path', [$this, 'ok']); + $this->request->url = '/path'; + $this->request->method = 'POST'; + + $this->check('OK'); + } + // Either GET or POST route public function testGetPostRoute() { @@ -135,6 +152,30 @@ class RouterTest extends PHPUnit\Framework\TestCase $this->check('OK'); } + public function testPutRouteShortcut() { + $this->router->put('/path', [$this, 'ok']); + $this->request->url = '/path'; + $this->request->method = 'PUT'; + + $this->check('OK'); + } + + public function testPatchRouteShortcut() { + $this->router->patch('/path', [$this, 'ok']); + $this->request->url = '/path'; + $this->request->method = 'PATCH'; + + $this->check('OK'); + } + + public function testDeleteRouteShortcut() { + $this->router->delete('/path', [$this, 'ok']); + $this->request->url = '/path'; + $this->request->method = 'DELETE'; + + $this->check('OK'); + } + // Test regular expression matching public function testRegEx() { @@ -403,4 +444,21 @@ class RouterTest extends PHPUnit\Framework\TestCase $this->request->url = '/client/user/123/abc'; $this->check('123abc'); } + + public function testGroupNestedRoutesWithCustomMethods() + { + $this->router->group('/client', function(Router $router) { + $router->group('/user', function(Router $router) { + $router->get('/@id', function ($id) { + echo $id; + }); + $router->post('/@id/@name', function ($id, $name) { + echo $id . $name; + }); + }); + }); + $this->request->url = '/client/user/123/abc'; + $this->request->method = 'POST'; + $this->check('123abc'); + } }