diff --git a/README.md b/README.md index da46842..a830f03 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The goal of this project is to create a router that is 100% compatible with the ### Features -- Basic routing (`GET`, `POST`, `PUT`, `DELETE`) with support for custom multiple verbs. +- Basic routing (`GET`, `POST`, `PUT`, `PATCH`, `UPDATE`, `DELETE`) with support for custom multiple verbs. - Regular Expression Constraints for parameters. - Named routes. - Generating url to routes. @@ -54,10 +54,10 @@ require_once 'routes.php'; /* * The default namespace for route-callbacks, so we don't have to specify it each time. - * Can be overwritten by using the namespace config option. + * Can be overwritten by using the namespace config option on your routes. */ -SimpleRouter::setDefaultNamespace('MyWebsite\Controller'); +SimpleRouter::setDefaultNamespace('MyWebsite'); // Start the routing SimpleRouter::start(); @@ -78,45 +78,74 @@ use Pecee\SimpleRouter\SimpleRouter; /* * This route will match the url /v1/services/answers/1/ - * The middleware is just a class that renders before the - * Controller or callback is loaded. This is useful for stopping - * the request, for instance if a user is not authenticated. + * controller or callback is loaded. + * + * This is useful for stopping the request, for + * instance if a user is not authenticated etc. */ -// Add CSRF support (if needed) -SimpleRouter::csrfVerifier(new \Pecee\Http\Middleware\BaseCsrfVerifier()); + +// Add your csrfVerifier here + +SimpleRouter::csrfVerifier(new \Demo\Middlewares\CsrfVerifier()); + +SimpleRouter::group(['middleware' => 'Middlewares\Site', 'exceptionHandler' => 'Handlers\CustomExceptionHandler'], function() { + + + SimpleRouter::get('/answers/{id}', 'ControllerAnswers@show', ['where' => ['id' => '[0-9]+']]); + + /** + * Using optional parameters + */ + SimpleRouter::get('/answers/{id?}', 'ControllerAnswers@show'); + + + /** + * This example will route url when matching the regular expression to the method. + * For example route: domain.com/ajax/music/world -> ControllerAjax@process (parameter: music/world) + */ + + SimpleRouter::all('/ajax', 'ControllerAjax@process')->setMatch('.*?\\/ajax\\/([A-Za-z0-9\\/]+)'); + + + /** + * Restful resource (see IRestController interface for available methods) + */ + + SimpleRouter::resource('/rest', 'ControllerRessource'); + + + /** + * Load the entire controller (where url matches method names - getIndex(), postIndex(), putIndex()). + * The url paths will determine which method to render. + * + * For example: + * + * GET /animals => getIndex() + * GET /animals/view => getView() + * POST /animals/save => postSave() + * + * etc. + */ + + SimpleRouter::controller('/animals', 'ControllerAnimals'); + + + /** + * Example of providing callback instead of Controller + */ + + SimpleRouter::post('/something', function() { + + die('Callback example'); + + }); + +}); SimpleRouter::get('/page/404', 'ControllerPage@notFound', ['as' => 'page.notfound']); -SimpleRouter::group(['prefix' => '/v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() { - - SimpleRouter::group(['prefix' => '/services', 'exceptionHandler' => '\MyProject\Handler\CustomExceptionHandler'], function() { - - SimpleRouter::get('/answers/{id}', 'ControllerAnswers@show')->where(['id' => '[0-9]+'); - - // Optional parameter - SimpleRouter::get('/answers/{id?}', 'ControllerAnswers@show'); - - /** - * This example will route url when matching the regular expression to the method. - * For example route: domain.com/ajax/music/world -> ControllerAjax@process (parameter: music/world) - */ - SimpleRouter::all('/ajax', 'ControllerAjax@process')->match('.*?\\/ajax\\/([A-Za-z0-9\\/]+)'); - - // Restful resource (see IRestController interface for available methods) - SimpleRouter::resource('/rest', 'ControllerRessource'); - - // Load the entire controller (where url matches method names - getIndex(), postIndex() etc) - SimpleRouter::controller('/controller', 'ControllerDefault'); - - // Example of providing callback instead of Controller - SimpleRouter::get('/something', function() { - die('Callback example'); - }); - - }); -}); ``` #### ExceptionHandler example @@ -126,32 +155,45 @@ This is a basic example of an ExceptionHandler implementation: ```php namespace Demo\Handlers; +use Pecee\Handlers\IExceptionHandler; use Pecee\Http\Request; +use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; use Pecee\SimpleRouter\RouterEntry; -class CustomExceptionHandler implements IExceptionHandler { +class CustomExceptionHandler implements IExceptionHandler +{ + public function handleError(Request $request, RouterEntry &$route = null, \Exception $error) + { - public function handleError( Request $request, RouterEntry $router = null, \Exception $error) { + /* You can use the exception handler to format errors depending on the request and type. */ - // If the error-code is 404; show another route which contains the page-not-found - if($error->getCode() === 404) { - - // Throw your custom 404-page view - // - or - - // load another route with our 404 page - // - or - - // you can return the $request object to ignore the error and continue on rendering the route. - - return $request->setUri(url('page.notfound')); - } + if (stripos($request->getUri(), '/api') !== false) { - // Output error as json if on api path. - if(stripos($request->getUri(), '/api') !== false) { - response()->json([ 'error' => $error->getMessage() ]); - } + response()->json([ + 'error' => $error->getMessage(), + 'code' => $error->getCode(), + ]); - // Otherwise default exception will be thrown by the router. - } + } + + /* The router will throw the NotFoundHttpException on 404 */ + if($error instanceof NotFoundHttpException) { + + /* + * Render your own custom 404-view, rewrite the request to another route, + * or simply return the $request object to ignore the error and continue on rendering the route. + * + * The code below will make the router render our page.notfound route. + */ + + $request->setUri(url('page.notfound')); + return $request; + + } + + throw $error; + + } } ``` @@ -162,9 +204,11 @@ Route groups may also be used to route wildcard sub-domains. Sub-domains may be ```php Route::group(['domain' => '{account}.myapp.com'], function () { + Route::get('user/{id}', function ($account, $id) { - // + // Do stuff... }); + }); ``` @@ -182,14 +226,16 @@ use \Pecee\SimpleRouter\RouterRoute; $router = RouterBase::getInstance(); $route = new RouterRoute('/answer/1', function() { + die('this callback will match /answer/1'); + }); -$route->setMiddleware('\HSWebserviceV1\Middleware\AuthMiddleware'); +$route->setMiddleware('\Demo\Middlewares\AuthMiddleware'); $route->setNamespace('MyWebsite'); $route->setPrefix('v1'); -// Add the route to the router +/* Add the route to the router */ $router->addRoute($route); ``` @@ -227,8 +273,25 @@ To simplify to use of simple-router functionality, we recommend you add these he ```php use Pecee\SimpleRouter\SimpleRouter; -function url($controller, $parameters = null, $getParams = null) { - SimpleRouter::getRoute($controller, $parameters, $getParams); +/** + * Get url for a route by using either name/alias, class or method name. + * + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * @param string|null $name + * @param string|array|null $parameters + * @param array $getParams + * @return string + */ +function url($name = null, $parameters = null, array $getParams = array()) { + SimpleRouter::getUrl($name, $parameters, $getParams); } /** @@ -294,13 +357,13 @@ Add the property ```except``` with an array of the urls to the routes you would Querystrings are ignored. ```php -use Pecee\Http\Middleware\BaseCsrfVerifier; +use Pecee\Http\Middlewares\BaseCsrfVerifier; class CsrfVerifier extends BaseCsrfVerifier { - + protected $except = [ '/companies/*', - '/api' + '/api', ]; } @@ -322,7 +385,7 @@ To interfere with the router, we create a class that inherits from ```RouterBoot use Pecee\Http\Request; use Pecee\SimpleRouter\RouterBootManager; -class CustomRouterRules extends RouterBootManager{ +class CustomRouterRules extends RouterBootManager { public function boot(Request $request) { @@ -389,7 +452,7 @@ The example below will cause the router to re-route the request with another url ```php -namespace demo\Middlewares; +namespace Demo\Middlewares; use Pecee\Http\Middleware\IMiddleware; use Pecee\Http\Request; @@ -398,7 +461,9 @@ use Pecee\SimpleRouter\RouterEntry; class CustomMiddleware implements Middleware { public function handle(Request $request, RouterEntry &$route) { + $request->setUri(url('home')); + } } @@ -415,7 +480,7 @@ If you wish to change the callback from outside, please have this in mind. **NOTE: Use this method if you want to load another controller. No additional middlewares or rules will be loaded.** ```php -namespace demo\Middlewares; +namespace Demo\Middlewares; use Pecee\Http\Middleware\IMiddleware; use Pecee\Http\Request; @@ -424,7 +489,9 @@ use Pecee\SimpleRouter\RouterEntry; class CustomMiddleware implements Middleware { public function handle(Request $request, RouterEntry &$route) { + $route->callback('DefaultController@home'); + } } @@ -435,22 +502,25 @@ class CustomMiddleware implements Middleware { We've added the `Input` class to easy access parameters from your Controller-classes. **Return single parameter value (matches both GET, POST, FILE):** + ```php $value = input()->get('name'); ``` **Return parameter object (matches both GET, POST, FILE):** + ```php $object = input()->getObject('name'); ``` **Return specific GET parameter (where name is the name of your parameter):** + ```php $object = input()->get->name; $object = input()->post->name; $object = input()->file->name; -// -- or -- +# -- or -- $object = input()->get->get($key, $defaultValue); $object = input()->post->get($key, $defaultValue); @@ -458,6 +528,7 @@ $object = input()->file->get($key, $defaultValue); ``` **Return all parameters:** + ```php // Get all $values = input()->all(); diff --git a/demo-project/app/Handlers/CustomExceptionHandler.php b/demo-project/app/Handlers/CustomExceptionHandler.php index 1d9171a..10a67f1 100644 --- a/demo-project/app/Handlers/CustomExceptionHandler.php +++ b/demo-project/app/Handlers/CustomExceptionHandler.php @@ -1,34 +1,44 @@ getUri(), '/api') !== false) { - header('content-type: application/json'); - echo json_encode([ + + response()->json([ 'error' => $error->getMessage(), - 'code' => $error->getCode() + 'code' => $error->getCode(), ]); - die(); + } - // else we just throw the error - if ($error->getCode() == 404) { + /* The router will throw the NotFoundHttpException on 404 */ + if($error instanceof NotFoundHttpException) { - // Return 404 path - $request->setUri('/404'); + /* + * Render your own custom 404-view, rewrite the request to another route, + * or simply return the $request object to ignore the error and continue on rendering the route. + * + * The code below will make the router render our page.notfound route. + */ + + $request->setUri(url('page.notfound')); return $request; } throw $error; + } } \ No newline at end of file diff --git a/demo-project/app/routes.php b/demo-project/app/routes.php index 2acdc52..4b5e3f7 100644 --- a/demo-project/app/routes.php +++ b/demo-project/app/routes.php @@ -9,11 +9,13 @@ Router::csrfVerifier(new \Demo\Middlewares\CsrfVerifier()); Router::group(['exceptionHandler' => 'Demo\Handlers\CustomExceptionHandler'], function () { - Router::get('/', 'DefaultController@index')->setAlias('home'); - Router::get('/contact', 'DefaultController@contact')->setAlias('contact'); - Router::get('/404', 'DefaultController@notFound')->setAlias('404'); - Router::basic('/companies', 'DefaultController@companies')->setAlias('companies'); - Router::basic('/companies/{id}', 'DefaultController@companies')->setAlias('companies'); + Router::get('/', 'DefaultController@index')->setName('home'); + + Router::get('/contact', 'DefaultController@contact')->setName('contact'); + + Router::get('/404', 'DefaultController@notFound')->setName('404'); + + Router::basic('/companies/{id?}', 'DefaultController@companies')->setName('companies'); // Api Router::group(['prefix' => '/api', 'middleware' => 'Demo\Middlewares\ApiVerification'], function () { diff --git a/src/Pecee/Controller/IRestController.php b/src/Pecee/Controllers/IRestController.php similarity index 94% rename from src/Pecee/Controller/IRestController.php rename to src/Pecee/Controllers/IRestController.php index e7994b9..ca391e0 100644 --- a/src/Pecee/Controller/IRestController.php +++ b/src/Pecee/Controllers/IRestController.php @@ -1,5 +1,5 @@ alias; + return $this->getName(); } /** - * Check if route has given alias. + * Returns the provided name for the router (first if multiple). + * @return string + */ + public function getName() + { + return $this->names[0]; + } + + /** + * Get route names + * @return array + */ + public function getNames() { + return $this->names; + } + + /** + * Check if route has given name. + * Alias for LoadableRoute::hasName(); + * + * @see LoadableRoute::hasName() + * @param $name + */ + public function hasAlias($name) + { + $this->hasName($name); + } + + /** + * Check if route has given name. * * @param string $name * @return bool */ - public function hasAlias($name) + public function hasName($name) { - if ($this->getAlias() !== null) { - if (is_array($this->getAlias()) === true) { - foreach ($this->getAlias() as $alias) { - if (strtolower($alias) === strtolower($name)) { - return true; - } - } - } - return strtolower($this->getAlias()) === strtolower($name); - } - - return false; + return (in_array($name, $this->names, false) !== false); } /** - * Set the url alias for easier getting the url route. - * @param string|array $alias + * Sets the router name, which makes it easier to obtain the url or router at a later point. + * Alias for LoadableRoute::setName(). + * + * @see LoadableRoute::setName() + * @param string|array $name * @return static */ - public function setAlias($alias) + public function setAlias($name) { - $this->alias = $alias; + return $this->setName($name); + } + + /** + * Sets the router name, which makes it easier to obtain the url or router at a later point. + * + * @param string $name + * @return static $this + */ + public function setName($name) + { + array_push($this->names, $name); + return $this; + } + + /** + * Set multiple names for the route + * + * @param array $names + * @return static $this + */ + public function setNames(array $names) { + $this->names = $names; return $this; } @@ -85,12 +131,16 @@ abstract class LoadableRoute extends RouterEntry implements ILoadableRoute */ public function merge(array $values) { - // Change as to alias if (isset($values['as'])) { - $this->setAlias($values['as']); + $this->setNames((array)$values['as']); + } + + if (isset($values['prefix'])) { + $this->setUrl($values['prefix'] . $this->getUrl()); } parent::merge($values); + return $this; } diff --git a/src/Pecee/SimpleRouter/RouterBase.php b/src/Pecee/SimpleRouter/RouterBase.php index 7c5dc19..f01f997 100644 --- a/src/Pecee/SimpleRouter/RouterBase.php +++ b/src/Pecee/SimpleRouter/RouterBase.php @@ -1,11 +1,12 @@ processingRoute = false; $this->request = new Request(); $this->response = new Response($this->request); - $this->routes = array(); - $this->bootManagers = array(); - $this->backStack = array(); - $this->controllerUrlMap = array(); - $this->exceptionHandlers = array(); + $this->routes = []; + $this->bootManagers = []; + $this->backStack = []; + $this->controllerUrlMap = []; + $this->exceptionHandlers = []; } /** @@ -134,25 +135,16 @@ class RouterBase return $route; } - protected function processRoutes(array $routes, array $settings = array(), array $prefixes = array(), RouterEntry $parent = null) + protected function processRoutes(array $routes, RouterGroup $group = null, RouterEntry $parent = null) { // Loop through each route-request /* @var $route RouterEntry */ foreach ($routes as $route) { - $newPrefixes = $prefixes; - $newSettings = $settings; - - if ($parent !== null) { - $route->setParent($parent); - } - - if (count($settings)) { - $route->merge($settings); - } - if ($route instanceof RouterGroup) { + $group = $route; + if ($route->getCallback() !== null && is_callable($route->getCallback())) { $this->processingRoute = true; @@ -160,33 +152,47 @@ class RouterBase $this->processingRoute = false; if ($route->matchRoute($this->request)) { - // Add ExceptionHandler + + /* Add exceptionhandlers */ if (count($route->getExceptionHandlers()) > 0) { $this->exceptionHandlers = array_merge($route->getExceptionHandlers(), $this->exceptionHandlers); } + } } + } - $newPrefixes[] = trim($route->getPrefix(), '/'); - $newSettings = array_merge($settings, $route->toArray()); + if($group !== null) { + + /* Add the parent group */ + $route->setGroup($group); + + } + + if ($parent !== null) { + + /* Add the parent route */ + $route->setParent($parent); + + /* Add/merge parent settings with child */ + $route->merge($parent->toArray()); } if ($route instanceof ILoadableRoute) { - if (count($prefixes)) { - $route->setUrl(trim(join('/', $prefixes) . $route->getUrl(), '/')); - } - + /* Add the route to the map, so we can find the active one when all routes has been loaded */ $this->controllerUrlMap[] = $route; } if (count($this->backStack) > 0) { + + /* Pop and grap the routes added when executing group callback earlier */ $backStack = $this->backStack; $this->backStack = []; - // Route any routes added to the backstack - $this->processRoutes($backStack, $newSettings, $newPrefixes, $route); + /* Route any routes added to the backstack */ + $this->processRoutes($backStack, $route, $group); } } } @@ -205,7 +211,7 @@ class RouterBase $this->request = $manager->boot($this->request); if (!($this->request instanceof Request)) { - throw new RouterException('Custom router bootmanager "' . get_class($manager) . '" must return instance of Request.'); + throw new HttpException('Bootmanager "' . get_class($manager) . '" must return instance of ' . Request::class, 500); } } } @@ -240,6 +246,7 @@ class RouterBase if ($this->request->getUri() !== $this->originalUrl && !in_array($this->request->getUri(), $this->routeRewrites)) { $this->routeRewrites[] = $this->request->getUri(); $this->routeRequest(true); + return; } @@ -255,12 +262,12 @@ class RouterBase $this->handleException($e); } - if ($routeNotAllowed) { - $this->handleException(new RouterException('Route or method not allowed', 403)); + if ($routeNotAllowed === true) { + $this->handleException(new HttpException('Route or method not allowed', 403)); } if ($this->loadedRoute === null) { - $this->handleException(new RouterException(sprintf('Route not found: %s', $this->request->getUri()), 404)); + $this->handleException(new NotFoundHttpException('Route not found: ' . $this->request->getUri(), 404)); } } @@ -272,7 +279,7 @@ class RouterBase $handler = new $handler(); if (!($handler instanceof IExceptionHandler)) { - throw new RouterException('Exception handler must implement the IExceptionHandler interface.'); + throw new HttpException('Exception handler must implement the IExceptionHandler interface.', 500); } $request = $handler->handleError($this->request, $this->loadedRoute, $e); @@ -280,17 +287,17 @@ class RouterBase if ($request !== null && $request->getUri() !== $this->originalUrl && !in_array($request->getUri(), $this->routeRewrites)) { $this->routeRewrites[] = $request->getUri(); $this->routeRequest(true); + return; } - } throw $e; } - public function arrayToParams(array $getParams = null, $includeEmpty = true) + public function arrayToParams(array $getParams = [], $includeEmpty = true) { - if (is_array($getParams) === true && count($getParams) > 0) { + if (count($getParams) > 0) { if ($includeEmpty === false) { $getParams = array_filter($getParams, function ($item) { @@ -304,151 +311,168 @@ class RouterBase return ''; } - protected function processUrl(LoadableRoute $route, $method = null, $parameters = null, $getParams = null) + protected function processUrl(LoadableRoute $route, $method = null, $parameters = null, array $getParams = []) { - $domain = ''; - $parent = $route->getParent(); + $url = ''; $parameters = (array)$parameters; - if ($parent !== null && $parent instanceof RouterGroup && count($parent->getDomains()) > 0) { - $domain = $parent->getDomains(); - $domain = '//' . $domain[0]; + if ($route->getGroup() !== null && count($route->getGroup()->getDomains()) > 0) { + $url .= '//' . $route->getGroup()->getDomains()[0]; } - $url = $domain . '/' . trim($route->getUrl(), '/'); + $url .= '/' . trim($route->getUrl(), '/'); if ($route instanceof IControllerRoute && $method !== null) { $url .= '/' . $method . '/'; if (count($parameters) > 0) { - $url .= join('/', (array)$parameters); + $url .= join('/', $parameters); } } else { - if ($parameters !== null && count($parameters) > 0) { - $params = array_merge($route->getParameters(), (array)$parameters); - } else { - $params = $route->getParameters(); - } + $params = array_merge($route->getParameters(), $parameters); - $otherParams = array(); + /* Url that contains parameters that aren't recognized */ + $unknownParams = []; + /* Let's parse the values of any {} parameter in the url */ foreach ($params as $param => $value) { $value = (isset($parameters[$param])) ? $parameters[$param] : $value; + /* Create the param string - {} */ $param1 = LoadableRoute::PARAMETER_MODIFIERS[0] . $param . LoadableRoute::PARAMETER_MODIFIERS[1]; + + /* Create the param string with the optional symbol - {?} */ $param2 = LoadableRoute::PARAMETER_MODIFIERS[0] . $param . LoadableRoute::PARAMETER_OPTIONAL_SYMBOL . LoadableRoute::PARAMETER_MODIFIERS[1]; if (stripos($url, $param1) !== false || stripos($url, $param) !== false) { $url = str_ireplace([$param1, $param2], $value, $url); } else { - $otherParams[$param] = $value; + $unknownParams[$param] = $value; } } - $url = rtrim($url, '/') . '/' . join('/', $otherParams); + $url = rtrim($url, '/') . '/' . join('/', $unknownParams); } - $url = rtrim($url, '/') . '/'; - - if ($getParams !== null) { - $url .= $this->arrayToParams($getParams); - } - - return $url; + return rtrim($url, '/') . '/' . $this->arrayToParams($getParams); } /** * Find route by alias, class, callback or method. * - * @param string $query + * @param string $name * @return LoadableRoute|null */ - public function findRoute($query) + public function findRoute($name) { /* @var $route LoadableRoute */ foreach ($this->controllerUrlMap as $route) { - // Check an alias exist, if the matches - use it - // Matches either Router alias or controller alias. - if ($route->hasAlias($query)) { + /* Check if the name matches with a name on the route. Should match either router alias or controller alias. */ + if ($route->hasName($name)) { return $route; } - // Direct match to controller - if ($route instanceof IControllerRoute) { - if (strtolower($route->getController()) === strtolower($query)) { - return $route; - } + /* Direct match to controller */ + if ($route instanceof IControllerRoute && strtolower($route->getController()) === strtolower($name)) { + return $route; } - // Using @ is most definitely a controller@method or alias@method - if (strpos($query, '@') !== false) { - list($controller, $method) = array_map('strtolower', explode('@', $query)); + /* Using @ is most definitely a controller@method or alias@method */ + if (strpos($name, '@') !== false) { + list($controller, $method) = array_map('strtolower', explode('@', $name)); if ($controller === strtolower($route->getClass()) && $method === strtolower($route->getMethod())) { return $route; } } - // Use callback if it's not a function - if (strpos($query, '@') !== false && strpos($route->getCallback(), '@') !== false && !is_callable($route->getCallback())) { + /* Check if callback matches (if it's not a function) */ + if (strpos($name, '@') !== false && strpos($route->getCallback(), '@') !== false && !is_callable($route->getCallback())) { - if (strtolower($query) === strtolower($route->getClass())) { + /* Check if the entire callback is matching */ + if (strtolower($route->getCallback()) === strtolower($name) || strpos($route->getCallback(), $name) === 0) { return $route; } - if (strtolower($route->getCallback()) === strtolower($query) || strpos($route->getCallback(), $query) === 0) { + /* Check if the class part of the callback matches (class@method) */ + if (strtolower($name) === strtolower($route->getClass())) { return $route; } - } } return null; } - public function getRoute($controller = null, $parameters = null, $getParams = null) + public function getRoute($name = null, $parameters = null, array $getParams = null) + { + return $this->getUrl($name, $parameters, $getParams); + } + + /** + * Get url for a route by using either name/alias, class or method name. + * + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * @param string|null $name + * @param string|array|null $parameters + * @param array|null $getParams + * @return string + */ + public function getUrl($name = null, $parameters = null, array $getParams = null) { if ($getParams !== null && is_array($getParams) === false) { throw new \InvalidArgumentException('Invalid type for getParams. Must be array or null'); } - // Return current route if no options has been specified - if ($controller === null && $parameters === null) { - - $getParams = ($getParams !== null) ? $getParams : $_GET; - $url = parse_url($this->request->getUri(), PHP_URL_PATH) . $this->arrayToParams($getParams); - - return $url; + if($getParams === null) { + $getParams = $_GET; } - // If nothing is defined and a route is loaded we use that - if ($controller === null && $this->loadedRoute !== null) { + /* Return current route if no options has been specified */ + if ($name === null && $parameters === null) { + return '/' . trim(parse_url($this->request->getUri(), PHP_URL_PATH), '/') . '/' . $this->arrayToParams($getParams); + } + + /* If nothing is defined and a route is loaded we use that */ + if ($name === null && $this->loadedRoute !== null) { return $this->processUrl($this->loadedRoute, $this->loadedRoute->getMethod(), $parameters, $getParams); } - $route = $this->findRoute($controller); + /* We try to find a match on the given name */ + $route = $this->findRoute($name); if ($route !== null) { return $this->processUrl($route, $route->getMethod(), $parameters, $getParams); } - // Using @ is most definitely a controller@method or alias@method - if (stripos($controller, '@') !== false) { - list($controller, $method) = explode('@', $controller); + /* Using @ is most definitely a controller@method or alias@method */ + if (stripos($name, '@') !== false) { + list($controller, $method) = explode('@', $name); + + /* Loop through all the routes to see if we can find a match */ /* @var $route LoadableRoute */ foreach ($this->controllerUrlMap as $route) { - if ($route->hasAlias($controller)) { + /* Check if the route contains the name/alias */ + if ($route->hasName($controller)) { return $this->processUrl($route, $method, $parameters, $getParams); } - // Match controllers either by: "alias @ method" or "controller@method" + /* Check if the route controller is equal to the name */ if ($route instanceof IControllerRoute && strtolower($route->getController()) === strtolower($controller)) { return $this->processUrl($route, $method, $parameters, $getParams); } @@ -456,19 +480,8 @@ class RouterBase } } - $url = [($controller === null) ? '/' : $controller]; - - if ($parameters !== null && count($parameters) > 0) { - $url = array_merge($url, (array)$parameters); - } - - $url = '/' . trim(join('/', $url), '/') . '/'; - - if ($getParams !== null) { - $url .= $this->arrayToParams($getParams); - } - - return $url; + /* No result so we assume that someone is using a hardcoded url and join everything together. */ + return '/' . trim(join('/', array_merge((array)$name, (array)$parameters)), '/') . '/' . $this->arrayToParams($getParams); } /** @@ -543,6 +556,7 @@ class RouterBase public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier) { $this->csrfVerifier = $csrfVerifier; + return $this; } diff --git a/src/Pecee/SimpleRouter/RouterController.php b/src/Pecee/SimpleRouter/RouterController.php index 83e9332..51be70f 100644 --- a/src/Pecee/SimpleRouter/RouterController.php +++ b/src/Pecee/SimpleRouter/RouterController.php @@ -1,8 +1,8 @@ getMethod() . ucfirst($controller[1]); if (!method_exists($class, $method)) { - throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); + throw new NotFoundHttpException(sprintf('Method %s does not exist in class %s', $method, $className), 404); } call_user_func_array(array($class, $method), $this->getParameters()); diff --git a/src/Pecee/SimpleRouter/RouterEntry.php b/src/Pecee/SimpleRouter/RouterEntry.php index 93ad206..771d53c 100644 --- a/src/Pecee/SimpleRouter/RouterEntry.php +++ b/src/Pecee/SimpleRouter/RouterEntry.php @@ -1,9 +1,10 @@ where) === true && isset($this->where[$parameter])) { $parameterRegex = $this->where[$parameter]; } @@ -80,8 +82,8 @@ abstract class RouterEntry } $parameterNames[] = [ - 'name' => $parameter, - 'required' => $required + 'name' => $parameter, + 'required' => $required, ]; $parameter = ''; @@ -97,17 +99,17 @@ abstract class RouterEntry $lastCharacter = $character; } - $parameterValues = array(); + $parameterValues = []; if (preg_match('/^' . $regex . '\/?$/is', $url, $parameterValues)) { - $parameters = array(); + $parameters = []; foreach ($parameterNames as $name) { $parameterValue = isset($parameterValues[$name['name']]) ? $parameterValues[$name['name']] : null; if ($name['required'] && $parameterValue === null) { - throw new RouterException('Missing required parameter ' . $name['name'], 404); + throw new HttpException('Missing required parameter ' . $name['name'], 404); } if ($name['required'] === false && $parameterValue === null) { @@ -130,12 +132,10 @@ abstract class RouterEntry $middleware = $this->loadClass($middleware); if (!($middleware instanceof IMiddleware)) { - throw new RouterException($middleware . ' must be instance of Middleware'); + throw new HttpException($middleware . ' must be instance of Middleware'); } - /* @var $class IMiddleware */ $middleware->handle($request, $route); - } } } @@ -144,12 +144,12 @@ abstract class RouterEntry { if ($this->getCallback() !== null && is_callable($this->getCallback())) { - // When the callback is a function + /* When the callback is a function */ call_user_func_array($this->getCallback(), $this->getParameters()); } else { - // When the callback is a method + /* When the callback is a method */ $controller = explode('@', $this->getCallback()); $className = $this->getNamespace() . '\\' . $controller[0]; @@ -157,14 +157,14 @@ abstract class RouterEntry $method = $controller[1]; if (!method_exists($class, $method)) { - throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); + throw new NotFoundHttpException(sprintf('Method %s does not exist in class %s', $method, $className), 404); } $parameters = array_filter($this->getParameters(), function ($var) { return ($var !== null); }); - call_user_func_array(array($class, $method), $parameters); + call_user_func_array([$class, $method], $parameters); return $class; } @@ -184,6 +184,7 @@ abstract class RouterEntry if (strpos($this->callback, '@') !== false) { return $this->callback; } + return 'function_' . md5($this->callback); } @@ -209,13 +210,33 @@ abstract class RouterEntry } /** - * @return RouterEntry + * @return LoadableRoute */ public function getParent() { return $this->parent; } + /** + * Get the group for the route. + * + * @return RouterGroup|null + */ + public function getGroup() + { + return $this->group; + } + + /** + * Set group + * + * @param RouterGroup $group + */ + public function setGroup(RouterGroup $group) + { + $this->group = $group; + } + /** * Set parent route * @@ -250,8 +271,10 @@ abstract class RouterEntry { if (strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); + return $tmp[1]; } + return null; } @@ -259,8 +282,10 @@ abstract class RouterEntry { if (strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); + return $tmp[0]; } + return null; } @@ -306,12 +331,14 @@ abstract class RouterEntry * @param string $namespace * @return static $this */ - public function setDefaultNamespace($namespace) { + public function setDefaultNamespace($namespace) + { $this->defaultNamespace = $namespace; return $this; } - public function getDefaultNamespace() { + public function getDefaultNamespace() + { return $this->defaultNamespace; } @@ -355,19 +382,19 @@ abstract class RouterEntry * @param array $options * @return static */ - public function where(array $options) + public function setWhere(array $options) { $this->where = $options; return $this; } /** - * Add regular expression match for url + * Add regular expression match for the entire route. * * @param string $regex * @return static */ - public function match($regex) + public function setMatch($regex) { $this->regex = $regex; return $this; @@ -380,7 +407,7 @@ abstract class RouterEntry */ public function toArray() { - $values = array(); + $values = []; if ($this->namespace !== null) { $values['namespace'] = $this->namespace; @@ -413,25 +440,25 @@ abstract class RouterEntry */ public function merge(array $values) { - if (isset($values['namespace'])) { + if (isset($values['namespace']) && $this->namespace === null) { $this->setNamespace($values['namespace']); } // Push middleware if multiple if (isset($values['middleware'])) { - $this->middlewares = array_merge((array)$values['middleware'], $this->middlewares); + $this->setMiddlewares(array_merge((array)$values['middleware'], $this->middlewares)); } if (isset($values['method'])) { - $this->setRequestMethods((array)$values['method']); + $this->setRequestMethods(array_merge($this->requestMethods, (array)$values['method'])); } if (isset($values['where'])) { - $this->where($values['where']); + $this->setWhere(array_merge($this->where, (array)$values['where'])); } if (isset($values['parameters'])) { - $this->setParameters($values['parameters']); + $this->setParameters(array_merge($this->parameters, (array)$values['parameters'])); } return $this; diff --git a/src/Pecee/SimpleRouter/RouterGroup.php b/src/Pecee/SimpleRouter/RouterGroup.php index ce693c1..c074b25 100644 --- a/src/Pecee/SimpleRouter/RouterGroup.php +++ b/src/Pecee/SimpleRouter/RouterGroup.php @@ -87,7 +87,7 @@ class RouterGroup extends RouterEntry public function merge(array $values) { if (isset($values['prefix'])) { - $this->setPrefix($values['prefix']); + $this->setPrefix($values['prefix'] . $this->prefix); } if (isset($values['exceptionHandler'])) { @@ -103,4 +103,20 @@ class RouterGroup extends RouterEntry return $this; } + /** + * Export route settings to array so they can be merged with another route. + * + * @return array + */ + public function toArray() + { + $values = array(); + + if ($this->prefix !== null) { + $values['prefix'] = $this->getPrefix(); + } + + return array_merge($values, parent::toArray()); + } + } \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterResource.php b/src/Pecee/SimpleRouter/RouterResource.php index 8847b1c..56ea3dc 100644 --- a/src/Pecee/SimpleRouter/RouterResource.php +++ b/src/Pecee/SimpleRouter/RouterResource.php @@ -1,8 +1,8 @@ getParameters()); diff --git a/src/Pecee/SimpleRouter/SimpleRouter.php b/src/Pecee/SimpleRouter/SimpleRouter.php index 0de00ff..810d560 100644 --- a/src/Pecee/SimpleRouter/SimpleRouter.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -7,8 +7,9 @@ */ namespace Pecee\SimpleRouter; -use Pecee\Exception\RouterException; use Pecee\Http\Middleware\BaseCsrfVerifier; +use Pecee\SimpleRouter\Exceptions\HttpException; +use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; class SimpleRouter { @@ -17,7 +18,8 @@ class SimpleRouter /** * Start/route request * - * @throws \Pecee\Exception\RouterException + * @throws HttpException + * @throws NotFoundHttpException */ public static function start() { @@ -138,17 +140,17 @@ class SimpleRouter * * @param array $settings * @param \Closure $callback - * @throws RouterException + * @throws \InvalidArgumentException * @return RouterGroup */ - public static function group(array $settings = array(), \Closure $callback) + public static function group(array $settings = [], \Closure $callback) { $group = new RouterGroup(); $group->setCallback($callback); $group->merge($settings); if (is_callable($callback) === false) { - throw new RouterException('Invalid callback provided. Only functions or methods supported'); + throw new \InvalidArgumentException('Invalid callback provided. Only functions or methods supported'); } static::router()->addRoute($group); @@ -277,16 +279,50 @@ class SimpleRouter } /** - * Get url by controller or alias. + * Get url for a route by using either name/alias, class or method name. * - * @param string $controller - * @param array|null $parameters + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * This method is an alias for SimpleRouter::getUrl(). + * + * @see SimpleRouter::getUrl() + * @param string|null $name + * @param string|array|null $parameters * @param array|null $getParams * @return string */ - public static function getRoute($controller = null, $parameters = null, $getParams = null) + public static function getRoute($name = null, $parameters = null, array $getParams = []) { - return static::router()->getRoute($controller, $parameters, $getParams); + return static::getUrl($name, $parameters, $getParams); + } + + /** + * Get url for a route by using either name/alias, class or method name. + * + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * @param string|null $name + * @param string|array|null $parameters + * @param array|null $getParams + * @return string + */ + public static function getUrl($name = null, $parameters = null, array $getParams = []) + { + return static::router()->getUrl($name, $parameters, $getParams); } /** diff --git a/test/Dummy/Handler/ExceptionHandler.php b/test/Dummy/Handler/ExceptionHandler.php index a2ab8df..6b61886 100644 --- a/test/Dummy/Handler/ExceptionHandler.php +++ b/test/Dummy/Handler/ExceptionHandler.php @@ -1,6 +1,6 @@ setUri('/my/fancy/url/1'); SimpleRouter::request()->setMethod('get'); + // Test array name SimpleRouter::get('/my/fancy/url/1', 'DummyController@start', ['as' => 'fancy1']); - SimpleRouter::get('/my/fancy/url/2', 'DummyController@start')->setAlias('fancy2'); + + // Test method name + SimpleRouter::get('/my/fancy/url/2', 'DummyController@start')->setName('fancy2'); + + // Test multiple names + SimpleRouter::get('/my/fancy/url/3', 'DummyController@start', ['as' => ['fancy3', 'fancy4']]); SimpleRouter::start(); - $this->assertTrue((SimpleRouter::getRoute('fancy1') === '/my/fancy/url/1/')); - $this->assertTrue((SimpleRouter::getRoute('fancy2') === '/my/fancy/url/2/')); + $this->assertEquals('/my/fancy/url/1/', SimpleRouter::getRoute('fancy1')); + $this->assertEquals('/my/fancy/url/2/', SimpleRouter::getRoute('fancy2')); + + $this->assertEquals('/my/fancy/url/3/', SimpleRouter::getRoute('fancy3')); + $this->assertEquals('/my/fancy/url/3/', SimpleRouter::getRoute('fancy4')); } diff --git a/test/RouterRouteTest.php b/test/RouterRouteTest.php index 5dfdc40..6207d3d 100644 --- a/test/RouterRouteTest.php +++ b/test/RouterRouteTest.php @@ -5,6 +5,7 @@ require_once 'Dummy/DummyController.php'; require_once 'Dummy/Handler/ExceptionHandler.php'; use Pecee\SimpleRouter\SimpleRouter as SimpleRouter; +use Pecee\SimpleRouter\Exceptions\NotFoundHttpException as NotFoundHttpException; class RouterRouteTest extends PHPUnit_Framework_TestCase { @@ -25,7 +26,7 @@ class RouterRouteTest extends PHPUnit_Framework_TestCase try { SimpleRouter::start(); } catch (\Exception $e) { - $found = ($e instanceof \Pecee\Exception\RouterException && $e->getCode() == 404); + $found = ($e instanceof NotFoundHttpException && $e->getCode() == 404); } $this->assertTrue($found); diff --git a/test/RouterUrlTest.php b/test/RouterUrlTest.php index 9515320..7dc317e 100644 --- a/test/RouterUrlTest.php +++ b/test/RouterUrlTest.php @@ -10,8 +10,8 @@ class RouterUrlTest extends PHPUnit_Framework_TestCase { protected $result = false; - protected function getUrl($controller = null, $parameters = null, $getParams = null) { - return SimpleRouter::getRoute($controller, $parameters, $getParams); + protected function getUrl($name = null, $parameters = null, array $getParams = []) { + return SimpleRouter::getRoute($name, $parameters, $getParams); } public function testUrls() @@ -75,8 +75,8 @@ class RouterUrlTest extends PHPUnit_Framework_TestCase // Should match /funny/man/ $this->assertEquals($this->getUrl('/funny/man'), '/funny/man/'); - // Should match /?jackdaniels=true - $this->assertEquals($this->getUrl('home', null, ['jackdaniels' => 'true']), '/?jackdaniels=true'); + // Should match /?jackdaniels=true&cola=yeah + $this->assertEquals($this->getUrl('home', null, ['jackdaniels' => 'true', 'cola' => 'yeah']), '/?jackdaniels=true&cola=yeah'); }