diff --git a/.gitignore b/.gitignore index 005d2e3..848a58c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -composer.lock \ No newline at end of file +composer.lock +vendor/ \ No newline at end of file diff --git a/README.md b/README.md index b4ce627..628aef2 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,13 @@ Simple, fast and yet powerful PHP router that is easy to get integrated and in a Add the latest version of Simple PHP Router running this command. ``` -composer require pecee/framework +composer require pecee/simple-router ``` +## Requirements + +- PHP 5.4 or greater + ## Notes The goal of this project is to create a router that is 100% compatible with the Laravel documentation, but as simple as possible and as easy to integrate and change as possible. @@ -34,7 +38,7 @@ In your ```index.php``` require your ```routes.php``` and call the ```routeReque This is an example of a basic ```index.php``` file: ```php -use \Pecee\SimpleRouter; +use \Pecee\SimpleRouter\SimpleRouter; require_once 'routes.php'; // change this to whatever makes sense in your project @@ -42,7 +46,7 @@ require_once 'routes.php'; // change this to whatever makes sense in your projec $defaultControllerNamespace = 'MyWebsite\\Controller'; // Do the routing -SimpleRouter::init($defaultControllerNamespace); +SimpleRouter::start($defaultControllerNamespace); ``` ## Adding routes @@ -51,6 +55,9 @@ This router is heavily inspired by the Laravel 5.* router, so anything you find ### Basic example +- ExceptionsHandlers must implement the `IExceptionHandler` interface. +- Middlewares must implement the `IMiddleware` interface. + ```php use Pecee\SimpleRouter\SimpleRouter; @@ -62,6 +69,9 @@ use Pecee\SimpleRouter\SimpleRouter; * the request, for instance if a user is not authenticated. */ +// Add CSRF support (if needed) +SimpleRouter::csrfVerifier(new \Pecee\Http\Middleware\BaseCsrfVerifier()); + SimpleRouter::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() { SimpleRouter::group(['prefix' => '/services', 'exceptionHandler' => '\MyProject\Handler\CustomExceptionHandler'], function() { @@ -160,23 +170,6 @@ class Router extends SimpleRouter { parent::start($defaultNamespace); } catch(\Exception $e) { - $route = RouterBase::getInstance()->getLoadedRoute(); - - $exceptionHandler = null; - - // Load and use exception-handler defined on group - - if($route && $route->getGroup()) { - $exceptionHandler = $route->getGroup()->getExceptionHandler(); - - if($exceptionHandler !== null) { - $class = new $exceptionHandler(); - $class->handleError(RouterBase::getInstance()->getRequest(), $route, $e); - } - } - - // Otherwise use the fallback default exceptions handler - if(self::$defaultExceptionHandler !== null) { $class = new self::$defaultExceptionHandler(); $class->handleError(RouterBase::getInstance()->getRequest(), $route, $e); @@ -264,7 +257,7 @@ SimpleRouter::csrfVerifier(new \Demo\Middleware\CsrfVerifier()); Sometimes it can be necessary to keep urls stored in the database, file or similar. In this example, we want the url ```/my-cat-is-beatiful``` to load the route ```/article/view/1``` which the router knows, because it's defined in the ```routes.php``` file. -To interfere with the router, we create a class that inherits from ```RouterBootManager```. This class will be loaded before any other rules in ```routes.php``` and allow us to "change" the current route, if any of our criteria are fulfilled (like comming from the url ```/my-cat-is-beatiful```). +To interfere with the router, we create a class that inherits from ```RouterBootManager```. This class will be loaded before any other rules in ```routes.php``` and allow us to "change" the current route, if any of our criteria are fulfilled (like coming from the url ```/my-cat-is-beatiful```). ```php @@ -332,6 +325,12 @@ $route->setClass('Example\MyCustomClass'); $route->setMethod('hello'); ``` +## Sites +This is some sites that uses the simple-router project in production. + +- [holla.dk](http://www.holla.dk) +- [ninjaimg.com](http://ninjaimg.com) + ## Documentation While I work on a better documentation, please refer to the Laravel 5 routing documentation here: diff --git a/composer.json b/composer.json index aa22e5d..7468967 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "require": { - + "php": ">=5.4.0" }, "require-dev": { "phpunit/phpunit": "4.7.7" diff --git a/src/Pecee/CsrfToken.php b/src/Pecee/CsrfToken.php index dec2409..98943ab 100644 --- a/src/Pecee/CsrfToken.php +++ b/src/Pecee/CsrfToken.php @@ -37,7 +37,7 @@ class CsrfToken { * @param $token */ public function setToken($token) { - setcookie(self::CSRF_KEY, $token, time() + 60 * 120, '/'); + setcookie(static::CSRF_KEY, $token, time() + 60 * 120, '/'); } /** @@ -46,7 +46,7 @@ class CsrfToken { */ public function getToken(){ if($this->hasToken()) { - return $_COOKIE[self::CSRF_KEY]; + return $_COOKIE[static::CSRF_KEY]; } return null; } @@ -56,7 +56,7 @@ class CsrfToken { * @return bool */ public function hasToken() { - return isset($_COOKIE[self::CSRF_KEY]); + return isset($_COOKIE[static::CSRF_KEY]); } } \ No newline at end of file diff --git a/src/Pecee/Handler/IExceptionHandler.php b/src/Pecee/Handler/IExceptionHandler.php new file mode 100644 index 0000000..045cab0 --- /dev/null +++ b/src/Pecee/Handler/IExceptionHandler.php @@ -0,0 +1,11 @@ +csrfToken = new CsrfToken(); + + // Generate or get the CSRF-Token from Cookie. + $this->token = (!$this->hasToken()) ? $this->generateToken() : $this->csrfToken->getToken(); } /** @@ -50,14 +53,14 @@ class BaseCsrfVerifier implements IMiddleware { if($request->getMethod() != 'get' && !$this->skip($request)) { - $token = (isset($_POST[self::POST_KEY])) ? $_POST[self::POST_KEY] : null; + $token = (isset($_POST[static::POST_KEY])) ? $_POST[static::POST_KEY] : null; // If the token is not posted, check headers for valid x-csrf-token if($token === null) { - $token = $request->getHeader(self::HEADER_KEY); + $token = $request->getHeader(static::HEADER_KEY); } - if( !$this->csrfToken->validate( $token ) ) { + if( !$this->csrfToken->validate($token) ) { throw new TokenMismatchException('Invalid csrf-token.'); } @@ -65,4 +68,22 @@ class BaseCsrfVerifier implements IMiddleware { } + public function generateToken() { + $token = $this->csrfToken->generateToken(); + $this->csrfToken->setToken($token); + return $token; + } + + public function hasToken() { + if($this->token != null) { + return true; + } + + return $this->csrfToken->hasToken(); + } + + public function getToken() { + return $this->token; + } + } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterBase.php b/src/Pecee/SimpleRouter/RouterBase.php index 3999f01..d73b67a 100644 --- a/src/Pecee/SimpleRouter/RouterBase.php +++ b/src/Pecee/SimpleRouter/RouterBase.php @@ -1,8 +1,8 @@ routes = array(); $this->backStack = array(); $this->controllerUrlMap = array(); - $this->baseCsrfVerifier = new BaseCsrfVerifier(); $this->request = Request::getInstance(); $this->bootManagers = array(); - - $csrf = new CsrfToken(); - $token = ($csrf->hasToken()) ? $csrf->getToken() : $csrf->generateToken(); - $csrf->setToken($token); } public function addRoute(RouterEntry $route) { @@ -73,13 +68,13 @@ class RouterBase { $newPrefixes = $prefixes; - if($route->getPrefix()) { - array_push($newPrefixes, rtrim($route->getPrefix(), '/')); + if($route->getPrefix() && trim($route->getPrefix(), '/') !== '') { + array_push($newPrefixes, trim($route->getPrefix(), '/')); } if(!($route instanceof RouterGroup)) { if(is_array($newPrefixes) && count($newPrefixes) && $backStack) { - $route->setUrl( join('/', $newPrefixes) . $route->getUrl() ); + $route->setUrl( '/' . join('/', $newPrefixes) . $route->getUrl() ); } $group = null; @@ -90,6 +85,12 @@ class RouterBase { if($route instanceof RouterGroup && is_callable($route->getCallback())) { $group = $route; + + // Load middleware on group if route matches + if($route->matchRoute($this->request)) { + $route->loadMiddleware($this->request); + } + $route->renderRoute($this->request); $mergedSettings = array_merge($settings, $route->getMergeableSettings()); } @@ -124,10 +125,7 @@ class RouterBase { // Verify csrf token for request if($this->baseCsrfVerifier !== null) { - /* @var $csrfVerifier BaseCsrfVerifier */ - $csrfVerifier = $this->baseCsrfVerifier; - $csrfVerifier = new $csrfVerifier(); - $csrfVerifier->handle($this->request); + $this->baseCsrfVerifier->handle($this->request); } // Loop through each route-request @@ -167,13 +165,18 @@ class RouterBase { $this->request->loadedRoute = $route; $route->loadMiddleware($this->request); - $this->request->loadedRoute->renderRoute($this->request); + try { + $this->request->loadedRoute->renderRoute($this->request); + } catch(\Exception $e) { + $this->handleException($e); + } + break; } } if($routeNotAllowed) { - throw new RouterException('Route or method not allowed', 403); + $this->handleException(new RouterException('Route or method not allowed', 403)); } if(!$this->request->loadedRoute) { @@ -181,6 +184,19 @@ class RouterBase { } } + protected function handleException(\Exception $e) { + if($this->request->loadedRoute !== null && $this->request->loadedRoute->exceptionHandler !== null) { + $handler = new $this->request->loadedRoute->exceptionHandler(); + if(!($handler instanceof IExceptionHandler)) { + throw new RouterException('Exception handler must implement the IExceptionHandler interface.'); + } + + $handler->handleError($this->request, $this->request->loadedRoute, $e); + } + + throw $e; + } + /** * @return string */ @@ -431,7 +447,6 @@ class RouterBase { $url = '/' . trim(join('/', $url), '/') . '/'; - if($getParams !== null && count($getParams)) { $url .= '?' . $this->arrayToParams($getParams); } @@ -446,4 +461,8 @@ class RouterBase { return self::$instance; } + public static function reset() { + self::$instance = null; + } + } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterController.php b/src/Pecee/SimpleRouter/RouterController.php index 14230b5..bcb5cdc 100644 --- a/src/Pecee/SimpleRouter/RouterController.php +++ b/src/Pecee/SimpleRouter/RouterController.php @@ -79,10 +79,12 @@ class RouterController extends RouterEntry { /** * @param string $url + * @return static */ public function setUrl($url) { $url = rtrim($url, '/') . '/'; $this->url = $url; + return $this; } /** @@ -94,9 +96,11 @@ class RouterController extends RouterEntry { /** * @param string $controller + * @return static */ public function setController($controller) { $this->controller = $controller; + return $this; } /** @@ -108,9 +112,11 @@ class RouterController extends RouterEntry { /** * @param string $method + * @return static */ public function setMethod($method) { $this->method = $method; + return $this; } } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterEntry.php b/src/Pecee/SimpleRouter/RouterEntry.php index 87e6b9f..bb6b598 100644 --- a/src/Pecee/SimpleRouter/RouterEntry.php +++ b/src/Pecee/SimpleRouter/RouterEntry.php @@ -93,7 +93,7 @@ abstract class RouterEntry { * @return self */ public function setPrefix($prefix) { - $this->prefix = '/' . trim($prefix, '/') . '/'; + $this->prefix = '/' . ltrim($prefix, '/'); return $this; } @@ -123,7 +123,7 @@ abstract class RouterEntry { } /** - * @return string + * @return string|array */ public function getMiddleware() { return $this->middleware; @@ -360,9 +360,7 @@ abstract class RouterEntry { } public function renderRoute(Request $request) { - if(is_object($this->getCallback()) && is_callable($this->getCallback())) { - // When the callback is a function call_user_func_array($this->getCallback(), $this->getParameters()); } else { diff --git a/src/Pecee/SimpleRouter/RouterGroup.php b/src/Pecee/SimpleRouter/RouterGroup.php index 015fb6b..231bb52 100644 --- a/src/Pecee/SimpleRouter/RouterGroup.php +++ b/src/Pecee/SimpleRouter/RouterGroup.php @@ -7,10 +7,6 @@ use Pecee\Http\Request; class RouterGroup extends RouterEntry { - public function __construct() { - parent::__construct(); - } - public function matchDomain(Request $request) { if($this->domain !== null) { diff --git a/src/Pecee/SimpleRouter/RouterResource.php b/src/Pecee/SimpleRouter/RouterResource.php index e2c1920..3ba17ad 100644 --- a/src/Pecee/SimpleRouter/RouterResource.php +++ b/src/Pecee/SimpleRouter/RouterResource.php @@ -109,10 +109,12 @@ class RouterResource extends RouterEntry { /** * @param string $url + * @return static */ public function setUrl($url) { $url = rtrim($url, '/') . '/'; $this->url = $url; + return $this; } /** @@ -124,9 +126,11 @@ class RouterResource extends RouterEntry { /** * @param string $controller + * @return static */ public function setController($controller) { $this->controller = $controller; + return $this; } } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterRoute.php b/src/Pecee/SimpleRouter/RouterRoute.php index 8b11afa..86c0a8d 100644 --- a/src/Pecee/SimpleRouter/RouterRoute.php +++ b/src/Pecee/SimpleRouter/RouterRoute.php @@ -2,7 +2,6 @@ namespace Pecee\SimpleRouter; -use Pecee\ArrayUtil; use Pecee\Http\Request; class RouterRoute extends RouterEntry { diff --git a/src/Pecee/SimpleRouter/SimpleRouter.php b/src/Pecee/SimpleRouter/SimpleRouter.php index 6a4d671..4469aa0 100644 --- a/src/Pecee/SimpleRouter/SimpleRouter.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -16,7 +16,7 @@ class SimpleRouter { /** * Start/route request * @param null $defaultNamespace - * @throws RouterException + * @throws \Pecee\Exception\RouterException */ public static function start($defaultNamespace = null) { $router = RouterBase::getInstance(); diff --git a/test/Dummy/DummyController.php b/test/Dummy/DummyController.php new file mode 100644 index 0000000..be3254b --- /dev/null +++ b/test/Dummy/DummyController.php @@ -0,0 +1,9 @@ +result = true; + } + + public function testGroup() { + \Pecee\SimpleRouter\RouterBase::reset(); + + $this->result = false; + + \Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/group'], $this->group()); + + try { + \Pecee\SimpleRouter\SimpleRouter::start(); + } catch(Exception $e) { + + } + + $this->assertTrue($this->result); + } + + public function testNestedGroup() { + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\Http\Request::getInstance()->setUri('/api/v1/test'); + + \Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/api'], function() { + \Pecee\SimpleRouter\SimpleRouter::group(['prefix' => '/v1'], function() { + \Pecee\SimpleRouter\SimpleRouter::get('/test', 'DummyController@start'); + }); + }); + + \Pecee\SimpleRouter\SimpleRouter::start(); + } + +} \ No newline at end of file diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php new file mode 100644 index 0000000..129df6d --- /dev/null +++ b/test/MiddlewareTest.php @@ -0,0 +1,36 @@ +setMethod('get'); + + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start', ['middleware' => 'DummyMiddleware']); + + try { + \Pecee\SimpleRouter\SimpleRouter::start(); + }catch(Exception $e) { + $this->assertTrue(($e instanceof MiddlewareLoadedException)); + return; + } + + throw new Exception('Middleware not loaded'); + + } + + + +} \ No newline at end of file diff --git a/test/RouterRouteTest.php b/test/RouterRouteTest.php new file mode 100644 index 0000000..01984ae --- /dev/null +++ b/test/RouterRouteTest.php @@ -0,0 +1,69 @@ +setMethod('get'); + + \Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start'); + \Pecee\SimpleRouter\SimpleRouter::start(); + } + + public function testPost() { + \Pecee\Http\Request::getInstance()->setMethod('post'); + + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\SimpleRouter\SimpleRouter::post('/my/test/url', 'DummyController@start'); + \Pecee\SimpleRouter\SimpleRouter::start(); + } + + public function testPut() { + \Pecee\Http\Request::getInstance()->setMethod('put'); + + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\SimpleRouter\SimpleRouter::put('/my/test/url', 'DummyController@start'); + \Pecee\SimpleRouter\SimpleRouter::start(); + } + + public function testDelete() { + \Pecee\Http\Request::getInstance()->setMethod('delete'); + + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\SimpleRouter\SimpleRouter::delete('/my/test/url', 'DummyController@start'); + \Pecee\SimpleRouter\SimpleRouter::start(); + + } + + public function testMethodNotAllowed() { + + \Pecee\SimpleRouter\RouterBase::reset(); + + \Pecee\Http\Request::getInstance()->setMethod('post'); + + \Pecee\SimpleRouter\SimpleRouter::get('/my/test/url', 'DummyController@start'); + + try { + \Pecee\SimpleRouter\SimpleRouter::start(); + } catch(\Exception $e) { + $this->assertEquals(403, $e->getCode()); + } + + } + +} \ No newline at end of file