diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..005d2e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +composer.lock \ No newline at end of file diff --git a/README.md b/README.md index 23f2d5f..3c2d319 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Simple PHP router Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router. +**Please note: this project has just been added and is still a work in progress. Please use with caution until a stable release version is available :)** + ## Installation Add the latest version pf Simple PHP Router to your ```composer.json``` @@ -15,6 +17,16 @@ Add the latest version pf Simple PHP Router to your ```composer.json``` } ``` +## Notes + +### Features currently "in-the-works" + +- Global Constraints +- Named Routes +- Sub-Domain Routing +- CSRF Protection +- Optinal/required parameters + ## Initialising the router In your ```index.php``` require your ```routes.php``` and call the ```routeRequest()``` method when all your custom routes has been loaded. This will trigger and do the actual routing of the requests. @@ -22,24 +34,25 @@ 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; + require_once 'routes.php'; // change this to whatever makes sense in your project - -// Initialise the router -$router = \Pecee\SimpleRouter::GetInstance(); - -// Do the actual routing -$router->routeRequest() + +// The apps default namespace (so we don't have to specify it each time we use MyController@home) +$defaultControllerNamespace = 'MyWebsite\\Controller'; + +// Do the routing +SimpleRouter::init($defaultControllerNamespace); ``` ## Adding routes Remember the ```routes.php``` file you required in your ```index.php```? This file will contain all your custom rules for routing. - This router is heavily inspired by the Laravel 5.* router, so anything you find in the Laravel documentation should work here as well. ### Basic example ```php -using \Pecee\Router; +use Pecee\SimpleRouter\SimpleRouter; /* * This route will match the url /v1/services/answers/1/ @@ -49,11 +62,23 @@ using \Pecee\Router; * the request, for instance if a user is not authenticated. */ -Router::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() { +SimpleRouter::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() { - Router::group(['prefix' => 'services'], function() { + SimpleRouter::group(['prefix' => 'services'], function() { - Router::get('/answers/{id}', 'ControllerAnswers@show'); + SimpleRouter::get('/answers/{id}', 'ControllerAnswers@show') + ->where(['id' => '[0-9]+'); + + // Resetful ressource + SimpleRouter::ressource('/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'); + }); }); }); @@ -61,13 +86,14 @@ Router::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMidd ### Doing it the object oriented (hardcore) way -The ```Router``` class is just a simple helper class that knows how to communicate with the ```SimpleRouter``` class. If you are up for a challenge, want the full control or simply just want to create your own ```Router``` helper class, this example is for you. +The ```SimpleRouter``` class referenced in the previous example, is just a simple helper class that knows how to communicate with the ```RouterBase``` class. +If you are up for a challenge, want the full control or simply just want to create your own ```Router``` helper class, this example is for you. ```php -use \Pecee\SimpleRouter; -use \Pecee\Router\RouterRoute; +use \Pecee\SimpleRouter\RouterBase; +use \Pecee\SimpleRouter\RouterRoute; -$router = SimpleRouter::GetInstance(); +$router = RouterBase::getInstance(); $route = new RouterRoute('/answer/1', function() { die('this callback will match /answer/1'); @@ -81,6 +107,78 @@ $route->setPrefix('v1'); $router->addRoute($route); ``` +This is a simple example of an integration into a framework. + +The framework has it's own ```Router``` class which inherits from the ```SimpleRouter``` class. This allows the framework to add custom functionality. + +```php +namespace MyProject; + +use Pecee\Handler\ExceptionHandler; +use Pecee\SimpleRouter\SimpleRouter; + +class Router extends SimpleRouter { + + protected static $exceptionHandlers = array(); + + public static function start() { + + Debug::getInstance()->add('Router initialised.'); + + // Load routes.php + $file = $_ENV['basePath'] . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'routes.php'; + if(file_exists($file)) { + require_once $file; + } + + // Init locale settings + Locale::getInstance(); + + // Set default namespace + $defaultNamespace = '\\'.Registry::getInstance()->get('AppName') . '\\Controller'; + + // Handle exceptions + try { + parent::start($defaultNamespace); + } catch(\Exception $e) { + /* @var $handler ExceptionHandler */ + foreach(self::$exceptionHandlers as $handler) { + $class = new $handler(); + $class->handleError($e); + } + + throw $e; + } + } + + public static function addExceptionHandler($handler) { + self::$exceptionHandlers[] = $handler; + } + +} +``` + +This is a basic example of a helper function for generating urls. + +```php +use Pecee\SimpleRouter\RouterBase; +function url($controller, $parameters = null, $getParams = null) { + RouterBase::getInstance()->getRoute($controller, $parameters, $getParams); +} +``` + +In ```routes.php``` we have added this route: + +```SimpleRouter::get('/item/{id}', 'myController@show');``` + +In the template we then call: + +```url('myController@show', ['id' => 22], ['category' => 'shoes']);``` + +Result url is: + +```/item/22?category=shoes ``` + ## 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 new file mode 100644 index 0000000..e7c280b --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "pecee/simple-router", + "description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.", + "keywords": [ "router", "routing", "laravel", "pecee" ], + "license": "MIT", + "support": { + "source": "https://github.com/skipperbent/simple-php-router/issues" + }, + "authors": [ + { + "name": "Simon Sessingø", + "email": "simon.sessingoe@gmail.com" + } + ], + "require": { + + }, + "require-dev": { + "phpunit/phpunit": "4.7.7" + }, + "autoload": { + "psr-4": { + "Pecee\\": "src/" + } + } +} \ No newline at end of file diff --git a/src/Pecee/Router/RouterGroup.php b/src/Pecee/Router/RouterGroup.php deleted file mode 100644 index 8e46449..0000000 --- a/src/Pecee/Router/RouterGroup.php +++ /dev/null @@ -1,21 +0,0 @@ -requestTypes) === 0 || in_array($requestMethod, $this->requestTypes)) { - return $this; - } - - // No match here, move on... - return null; - } - -} \ No newline at end of file diff --git a/src/Pecee/Router/RouterRoute.php b/src/Pecee/Router/RouterRoute.php deleted file mode 100644 index c3bd5ea..0000000 --- a/src/Pecee/Router/RouterRoute.php +++ /dev/null @@ -1,106 +0,0 @@ -url = $url; - $this->callback = $callback; - - $this->settings['aliases'] = array(); - - // Set default namespace - $this->namespace = Registry::GetInstance()->get(SimpleRouter::SETTINGS_APPNAME, false) . '\\' . 'Controller'; - } - - public function getRoute($requestMethod, &$url) { - - // Check if request method is allowed - if(count($this->requestTypes) === 0 || in_array($requestMethod, $this->requestTypes)) { - - $url = explode('/', trim($url, '/')); - $route = explode('/', trim($this->url, '/')); - - // Check if url parameter count matches - if(count($url) === count($route)) { - - $parameters = array(); - - $matches = true; - - // Check if url matches - foreach($route as $i => $path) { - $parameter = $this->parseParameter($path); - - // Check if parameter of path matches, otherwise quit.. - if(is_null($parameter) && strtolower($path) != strtolower($url[$i])) { - $matches = false; - break; - } - - // Save parameter if we have one - if($parameter) { - $parameters[$parameter] = $url[$i]; - } - } - - // This route matches - if($matches) { - $this->parameters = $parameters; - return $this; - } - - } - } - - // No match here, move on... - return null; - } - - /** - * @return string - */ - public function getUrl() { - return $this->url; - } - - /** - * @param string $url - * @return self - */ - public function setUrl($url) { - $this->url = $url; - return $this; - } - - /** - * @param array $aliases - * @return self - */ - public function setAliases(array $aliases) { - $this->aliases = $aliases; - return $this; - } - - /** - * Add alias - * - * @param $alias - * @return self - */ - public function addAlias($alias) { - $this->aliases[] = $alias; - return $this; - } - - public function getAliases() { - $this->aliases; - } -} \ No newline at end of file diff --git a/src/Pecee/Router/SimpleRouter.php b/src/Pecee/Router/SimpleRouter.php deleted file mode 100644 index 328a222..0000000 --- a/src/Pecee/Router/SimpleRouter.php +++ /dev/null @@ -1,149 +0,0 @@ -appName = Registry::GetInstance()->get(self::SETTINGS_APPNAME, false); - if (!$this->appName) { - throw new RouterException('"AppName" registry key not defined!', 2); - } - - $this->routes = array(); - $this->backstack = array(); - $this->requestUri = rtrim($_SERVER['REQUEST_URI'], '/'); - $this->requestMethod = strtolower(isset($_GET['_method']) ? $_GET['_method'] : $_SERVER['REQUEST_METHOD']); - } - - public function addRoute(RouterEntry $route) { - if($this->currentRoute !== null) { - $this->backstack[] = $route; - } else { - $this->routes[] = $route; - } - } - - public function route($url, $callback) { - $route = new RouterRoute($url, $callback); - $this->addRoute($route); - return $route; - } - - protected function loadClass($name) { - if(!class_exists($name)) { - throw new RouterException(sprintf('Class %s does not exist', $name)); - } - - return new $name(); - } - - public function renderRoute(RouterEntry $route) { - $this->currentRoute = $route; - - // Load middlewares if any - if($route->getMiddleware()) { - $this->loadClass($route->getMiddleware()); - } - - if(is_object($route->getCallback()) && is_callable($route->getCallback())) { - - // When the callback is a function - call_user_func_array($route->getCallback(), $route->getParameters()); - - } else if(stripos($route->getCallback(), '@') > 0) { - // When the callback is a method - - $controller = explode('@', $route->getCallback()); - $class = $route->getNamespace() . '\\' . $controller[0]; - - $class = $this->loadClass($class); - - $this->loadedClass = $class; - - $method = $controller[1]; - - if(!method_exists($class, $method)) { - throw new RouterException(sprintf('Method %s does not exist', $method)); - } - - call_user_func_array(array($class, $method), $route->getParameters()); - } - } - - protected function renderBackstack(array $routes, &$settings, &$prefixes) { - // Loop through each route-request - /* @var $route RouterEntry */ - foreach($routes as $route) { - - $settings = array_merge($settings, $route->getMergeableSettings()); - if($route->getPrefix()) { - array_push($prefixes, $route->getPrefix()); - } - - // If the route is a group - if($route instanceof RouterRoute) { - $route->setSettings($settings); - $route->setUrl( '/' . join('/', $prefixes) . $route->getUrl() ); - } - - // Stop if the route matches - $route = $route->getRoute($this->requestMethod, $this->requestUri); - if($route) { - $this->renderRoute($route); - } - - // Remove itself from backstack - array_shift($this->backstack); - - // Route any routes added to the backstack - $this->renderBackstack($this->backstack, $settings, $prefixes); - } - } - - public function routeRequest() { - // Loop through each route-request - /* @var $route RouterEntry */ - foreach($this->routes as $route) { - - // Reset variables - $settings = array(); - $prefixes = array(); - - $settings = array_merge($settings, $route->getMergeableSettings()); - - if($route->getPrefix()) { - array_push($prefixes, $route->getPrefix()); - } - - // Stop if the route matches - $route = $route->getRoute($this->requestMethod, $this->requestUri); - if($route) { - $this->renderRoute($route); - } - - // Route any routes added to the backstack - $this->renderBackstack($this->backstack, $settings, $prefixes); - } - } - - public static function GetInstance() { - if(self::$instance === null) { - self::$instance = new self(); - } - return self::$instance; - } - -} \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/IRouteEntry.php b/src/Pecee/SimpleRouter/IRouteEntry.php new file mode 100644 index 0000000..d13e752 --- /dev/null +++ b/src/Pecee/SimpleRouter/IRouteEntry.php @@ -0,0 +1,7 @@ +routes = array(); + $this->backstack = array(); + $this->controllerUrlMap = array(); + $this->requestUri = $_SERVER['REQUEST_URI']; + $this->requestMethod = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : strtolower($_SERVER['REQUEST_METHOD']); + } + + public function addRoute(RouterEntry $route) { + if($this->currentRoute !== null) { + $this->backstack[] = $route; + } else { + $this->routes[] = $route; + } + } + + protected function processRoutes(array $routes, array $settings = array(), array $prefixes = array(), $backstack = false) { + // Loop through each route-request + + /* @var $route RouterEntry */ + foreach($routes as $route) { + + if($this->defaultNamespace) { + $namespace = null; + + if ($route->getNamespace()) { + $namespace = $this->defaultNamespace . '\\' . $route->getNamespace(); + } else { + $namespace = $this->defaultNamespace; + } + + $route->setNamespace($namespace); + } + + $newPrefixes = $prefixes; + $mergedSettings = array_merge($settings, $route->getMergeableSettings()); + if($route->getPrefix()) { + array_push($newPrefixes, rtrim($route->getPrefix(), '/')); + } + $route->addSettings($mergedSettings); + + if(!($route instanceof RouterGroup)) { + if(is_array($newPrefixes) && count($newPrefixes) && $backstack) { + $route->setUrl( join('/', $newPrefixes) . $route->getUrl() ); + } + + $this->controllerUrlMap[] = $route; + } + + $this->currentRoute = $route; + if($route instanceof RouterGroup && is_callable($route->getCallback())) { + $route->renderRoute($this->requestMethod); + } + $this->currentRoute = null; + + if(count($this->backstack)) { + $backstack = $this->backstack; + $this->backstack = array(); + + // Route any routes added to the backstack + $this->processRoutes($backstack, $mergedSettings, $newPrefixes, true); + } + } + } + + public function routeRequest() { + // Loop through each route-request + + $this->processRoutes($this->routes); + + // Make sure the urls is in the right order when comparing + usort($this->controllerUrlMap, function($a, $b) { + return strcmp($b->getUrl(), $a->getUrl()); + }); + + foreach($this->controllerUrlMap as $route) { + $routeMatch = $route->matchRoute($this->requestMethod, rtrim($this->requestUri, '/') . '/'); + + if($routeMatch && !($routeMatch instanceof RouterGroup)) { + $this->loadedRoute = $routeMatch; + $routeMatch->renderRoute($this->requestMethod); + break; + } + } + + if(!$this->loadedRoute) { + throw new RouterException(sprintf('Route not found: %s', $this->requestUri), 404); + } + } + + /** + * @return string + */ + public function getDefaultNamespace(){ + return $this->defaultNamespace; + } + + /** + * @param string $defaultNamespace + */ + public function setDefaultNamespace($defaultNamespace) { + $this->defaultNamespace = $defaultNamespace; + } + + /** + * @return RouterEntry + */ + public function getLoadedRoute() { + if(!($this->loadedRoute instanceof RouterGroup)) { + return $this->loadedRoute; + } + return null; + } + + /** + * @return string + */ + public function getRequestMethod() { + return $this->requestMethod; + } + + /** + * @return string + */ + public function getRequestUri() { + return $this->requestUri; + } + + /** + * @return array + */ + public function getBackstack() { + return $this->backstack; + } + + /** + * @return RouterEntry + */ + public function getCurrentRoute(){ + return $this->currentRoute; + } + + /** + * @return array + */ + public function getRoutes(){ + return $this->routes; + } + + protected function processUrl($route, $method = null, $parameters = null, $getParams = null) { + $url = rtrim($route->getUrl(), '/') . '/'; + + if($method !== null) { + $url .= $method . '/'; + } + + if($route instanceof RouterController || $route instanceof RouterRessource) { + if(count($parameters)) { + $url .= join('/', $parameters); + } + } else { + /* @var $route RouterEntry */ + $params = $route->getParameters(); + if(count($params)) { + $i = 0; + foreach($params as $param => $value) { + $value = (isset($parameters[$param])) ? $parameters[$param] : $value; + $url = str_ireplace('{' . $param. '}', $value, $route->getUrl()); + $i++; + } + } + } + + $p = ''; + if($getParams !== null && count($getParams)) { + $p = '?'.Url::arrayToParams($getParams); + } + + $url .= $p; + + return $url; + } + + public function getRoute($controller = null, $parameters = null, $getParams = null) { + + if($parameters !== null && !is_array($parameters)) { + throw new \InvalidArgumentException('Invalid type for parameter. Must be array or null'); + } + + if($getParams !== null && !is_array($getParams)) { + throw new \InvalidArgumentException('Invalid type for getParams. Must be array or null'); + } + + $c = ''; + $method = null; + + /* @var $route RouterRoute */ + foreach($this->controllerUrlMap as $route) { + + if($route instanceof RouterRoute && !is_callable($route->getCallback()) && stripos($route->getCallback(), '@') !== false) { + $c = $route->getCallback(); + } else if($route instanceof RouterController || $route instanceof RouterRessource) { + $c = $route->getController(); + } + + if($c === $controller || strpos($c, $controller) === 0) { + if(stripos($c, '@') !== false) { + $tmp = explode('@', $route->getCallback()); + $method = strtolower($tmp[1]); + } + return $this->processUrl($route, $method, $parameters, $getParams); + } + } + + // No match has yet been found, let's try to guess what url that should be returned + foreach($this->controllerUrlMap as $route) { + if($route instanceof RouterRoute && !is_callable($route->getCallback()) && stripos($route->getCallback(), '@') !== false) { + $c = $route->getCallback(); + + if(stripos($controller, '@') !== false) { + $tmp = explode('@', $controller); + $c = $tmp[0]; + } + + } else if($route instanceof RouterController || $route instanceof RouterRessource) { + $c = $route->getController(); + } + + if(stripos($controller, '@') !== false) { + $tmp = explode('@', $controller); + $controller = $tmp[0]; + $method = $tmp[1]; + } + + if($controller == $c) { + return $this->processUrl($route, $method, $parameters, $getParams); + } + } + + // Nothing found - return current route + if($this->loadedRoute) { + $getParams = ($getParams === null) ? array() : $getParams; + $params = ($this->loadedRoute->getParameters() == null) ? array() : $this->loadedRoute->getParameters(); + $parameters = ($parameters === null) ? array() : $parameters; + return $this->processUrl($this->loadedRoute, null, array_merge($params, $parameters), array_merge($_GET, $getParams)); + } + + return '/'; + } + + public static function getInstance() { + if(self::$instance === null) { + self::$instance = new static(); + } + return self::$instance; + } + +} \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterController.php b/src/Pecee/SimpleRouter/RouterController.php new file mode 100644 index 0000000..d97c3bd --- /dev/null +++ b/src/Pecee/SimpleRouter/RouterController.php @@ -0,0 +1,88 @@ +url = $url; + $this->controller = $controller; + } + + public function matchRoute($requestMethod, $url) { + $url = parse_url($url); + $url = rtrim($url['path'], '/') . '/'; + + if(strtolower($url) == strtolower($this->url) || stripos($url, $this->url) === 0) { + + $strippedUrl = trim(str_ireplace($this->url, '/', $url), '/'); + + $path = explode('/', $strippedUrl); + + if(count($path)) { + + $method = (!isset($path[0]) || trim($path[0]) === '') ? self::DEFAULT_METHOD : $path[0]; + $this->method = $method; + + array_shift($path); + $this->parameters = $path; + + // Set callback + $this->setCallback($this->controller . '@' . $this->method); + + return $this; + } + } + return null; + } + + /** + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * @param string $url + */ + public function setUrl($url) { + $url = rtrim($url, '/') . '/'; + $this->url = $url; + } + + /** + * @return string + */ + public function getController() { + return $this->controller; + } + + /** + * @param string $controller + */ + public function setController($controller) { + $this->controller = $controller; + } + + /** + * @return string + */ + public function getMethod() { + return $this->method; + } + + /** + * @param string $method + */ + public function setMethod($method) { + $this->method = $method; + } + +} \ No newline at end of file diff --git a/src/Pecee/Router/RouterEntry.php b/src/Pecee/SimpleRouter/RouterEntry.php similarity index 58% rename from src/Pecee/Router/RouterEntry.php rename to src/Pecee/SimpleRouter/RouterEntry.php index f5ecb06..3dffcb5 100644 --- a/src/Pecee/Router/RouterEntry.php +++ b/src/Pecee/SimpleRouter/RouterEntry.php @@ -1,6 +1,6 @@ settings = array(); - $this->requestTypes = array(); $this->parameters = array(); + $this->parametersRegex = array(); } - protected function parseParameter($path) { - $parameters = array(); - - preg_match('/{([A-Za-z\-\_]*?)}/is', $path, $parameters); - - if(isset($parameters[1]) && count($parameters[1]) > 0) { - return $parameters[1]; + protected function loadClass($name) { + if(!class_exists($name)) { + throw new RouterException(sprintf('Class %s does not exist', $name)); } - return null; + return new $name(); + } + + /** + * Returns callback name/identifier for the current route based on the callback. + * Useful if you need to get a unique identifier for the loaded route, for instance + * when using translations etc. + * + * @return string + */ + public function getIdentifier() { + if(strpos($this->callback, '@') !== false) { + return $this->callback; + } + return 'function_' . md5($this->callback); } /** @@ -55,51 +65,12 @@ abstract class RouterEntry { return $this->callback; } - /** - * Add request type - * - * @param $type - * @return self - * @throws RouterException - */ - public function addRequestType($type) { - if(!in_array($type, self::$allowedRequestTypes)) { - throw new RouterException('Invalid request method: ' . $type); - } - - $this->requestTypes[] = $type; - return $this; - } - - /** - * @return mixed - */ - public function getRequestTypes() { - return $this->requestTypes; - } - - /** - * @return mixed - */ - public function getParameters(){ - return $this->parameters; - } - - /** - * @param mixed $parameters - * @return self - */ - public function setParameters($parameters) { - $this->parameters = $parameters; - return $this; - } - /** * @param string $prefix * @return self */ public function setPrefix($prefix) { - $this->prefix = trim($prefix, '/'); + $this->prefix = '/' . trim($prefix, '/') . '/'; return $this; } @@ -149,6 +120,33 @@ abstract class RouterEntry { return $this->settings; } + /** + * @return mixed + */ + public function getParameters(){ + return $this->parameters; + } + + /** + * @param mixed $parameters + * @return self + */ + public function setParameters($parameters) { + $this->parameters = $parameters; + return $this; + } + + /** + * Add regular expression parameter match + * + * @param array $options + * @return self + */ + public function where(array $options) { + $this->parametersRegex = array_merge($this->parametersRegex, $options); + return $this; + } + /** * Get settings that are allowed to be inherited by child routes. * @@ -168,12 +166,26 @@ abstract class RouterEntry { return $settings; } + /** + * @param array $settings + * @return self + */ + public function addSettings(array $settings) { + array_merge($this->settings, $settings); + return $this; + } + /** * @param array $settings * @return self */ public function setSettings($settings) { $this->settings = $settings; + + if($settings['prefix']) { + $this->setPrefix($settings['prefix']); + } + return $this; } @@ -197,6 +209,35 @@ abstract class RouterEntry { $this->settings[$name] = $value; } - abstract function getRoute($requestMethod, &$url); + public function renderRoute($requestMethod) { + // Load middleware + if($this->getMiddleware()) { + $this->loadClass($this->getMiddleware()); + } + + if(is_object($this->getCallback()) && is_callable($this->getCallback())) { + + // When the callback is a function + call_user_func_array($this->getCallback(), $this->getParameters()); + } else { + // When the callback is a method + $controller = explode('@', $this->getCallback()); + $className = $this->getNamespace() . '\\' . $controller[0]; + $class = $this->loadClass($className); + $method = $requestMethod . ucfirst($controller[1]); + + if (!method_exists($class, $method)) { + throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); + } + + call_user_func_array(array($class, $method), $this->getParameters()); + + return $class; + } + + return null; + } + + abstract function matchRoute($requestMethod, $url); } \ No newline at end of file diff --git a/src/Pecee/Router/RouterException.php b/src/Pecee/SimpleRouter/RouterException.php similarity index 62% rename from src/Pecee/Router/RouterException.php rename to src/Pecee/SimpleRouter/RouterException.php index 815d0ab..c8ca241 100644 --- a/src/Pecee/Router/RouterException.php +++ b/src/Pecee/SimpleRouter/RouterException.php @@ -1,3 +1,3 @@ prefix) || stripos($url, $this->prefix) === 0) { + + $hasAccess = (!$this->method); + + if($this->method) { + if(is_array($this->method)) { + $hasAccess = (in_array($requestMethod, $this->method)); + } else { + $hasAccess = strtolower($this->method) == strtolower($requestMethod); + } + } + + if(!$hasAccess) { + throw new RouterException('Method not allowed'); + } + + return $this; + } + + // No match here, move on... + return null; + } + +} \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterRessource.php b/src/Pecee/SimpleRouter/RouterRessource.php new file mode 100644 index 0000000..683400c --- /dev/null +++ b/src/Pecee/SimpleRouter/RouterRessource.php @@ -0,0 +1,155 @@ +url = $url; + $this->controller = $controller; + $this->postMethod = strtolower(($_SERVER['REQUEST_METHOD'] != 'GET') ? 'post' : 'get'); + } + + public function renderRoute($requestMethod) { + // Load middleware + if($this->getMiddleware()) { + $this->loadClass($this->getMiddleware()); + } + + if(is_object($this->getCallback()) && is_callable($this->getCallback())) { + // When the callback is a function + call_user_func_array($this->getCallback(), $this->getParameters()); + } else { + // When the callback is a method + $controller = explode('@', $this->getCallback()); + $className = $this->getNamespace() . '\\' . $controller[0]; + $class = $this->loadClass($className); + $method = strtolower($controller[1]); + + if (!method_exists($class, $method)) { + throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); + } + + call_user_func_array(array($class, $method), $this->getParameters()); + + return $class; + } + + return null; + } + + protected function call($method, $parameters) { + $this->setCallback($this->controller . '@' . $method); + $this->parameters = $parameters; + return $this; + } + + public function matchRoute($requestMethod, $url) { + $url = parse_url($url); + $url = rtrim($url['path'], '/') . '/'; + + if(strtolower($url) == strtolower($this->url) || stripos($url, $this->url) === 0) { + $url = rtrim($url, '/'); + + $strippedUrl = trim(substr($url, strlen($this->url)), '/'); + $path = explode('/', $strippedUrl); + + $args = $path; + $action = ''; + + if(count($args)) { + $action = $args[0]; + array_shift($args); + } + + if (count($path)) { + + // Delete + if($requestMethod === self::REQUEST_TYPE_DELETE && $this->postMethod === self::REQUEST_TYPE_POST) { + return $this->call('destroy', $args); + } + + // Update + if(in_array($requestMethod, array('put', 'patch')) && $this->postMethod === self::REQUEST_TYPE_POST) { + return $this->call('update', array_merge(array($action), $args)); + } + + // Edit + if(isset($args[0]) && strtolower($args[0]) === 'edit' && $this->postMethod === self::REQUEST_TYPE_GET) { + return $this->call('edit', array_merge(array($action), array_slice($args, 1))); + } + + // Create + if(strtolower($action) === 'create' && $requestMethod === self::REQUEST_TYPE_GET) { + return $this->call('create', $args); + } + + // Save + if($this->postMethod === self::REQUEST_TYPE_POST) { + return $this->call('store', $args); + } + + // Show + if($action && $this->postMethod === self::REQUEST_TYPE_GET) { + return $this->call('show', array_merge(array($action), $args)); + } + + // Index + return $this->call('index', $args); + } + } + + return null; + } + + /** + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * @param string $url + */ + public function setUrl($url) { + $url = rtrim($url, '/') . '/'; + $this->url = $url; + } + + /** + * @return string + */ + public function getController() { + return $this->controller; + } + + /** + * @param string $controller + */ + public function setController($controller) { + $this->controller = $controller; + } + + /** + * @return string + */ + public function getMethod() { + return $this->method; + } + + /** + * @param string $method + */ + public function setMethod($method) { + $this->method = $method; + } + +} \ No newline at end of file diff --git a/src/Pecee/SimpleRouter/RouterRoute.php b/src/Pecee/SimpleRouter/RouterRoute.php new file mode 100644 index 0000000..580a94c --- /dev/null +++ b/src/Pecee/SimpleRouter/RouterRoute.php @@ -0,0 +1,177 @@ +setUrl($url); + $this->setCallback($callback); + + $this->settings['aliases'] = array(); + $this->requestTypes = array(); + } + + protected function parseParameters($url) { + $parameters = array(); + + preg_match_all('/{([A-Za-z\-\_]*?)}/is', $url, $parameters); + + if(isset($parameters[1]) && count($parameters[1]) > 0) { + return $parameters[1]; + } + + return null; + } + + protected function parseParameter($path) { + $parameters = array(); + + preg_match('/{([A-Za-z\-\_]*?)}/is', $path, $parameters); + + if(isset($parameters[1]) && count($parameters[1]) > 0) { + return $parameters[1]; + } + + return null; + } + + public function matchRoute($requestMethod, $url) { + + // Check if request method is allowed + if(count($this->requestTypes) === 0 || in_array($requestMethod, $this->requestTypes)) { + + $url = parse_url($url); + $url = $url['path']; + + $url = explode('/', trim($url, '/')); + $route = explode('/', trim($this->url, '/')); + + // Check if url parameter count matches + if(count($url) === count($route)) { + + $parameters = array(); + + $matches = true; + + // Check if url matches + foreach($route as $i => $path) { + $parameter = $this->parseParameter($path); + + // Check if parameter of path matches, otherwise quit.. + if(is_null($parameter) && strtolower($path) != strtolower($url[$i])) { + $matches = false; + break; + } + + // Save parameter if we have one + if($parameter) { + $parameterValue = $url[$i]; + $regex = (isset($this->parametersRegex[$parameter]) ? $this->parametersRegex[$parameter] : null); + + if($regex !== null) { + // Use the regular expression rule provided to filter the value + $matches = array(); + preg_match('/'.$regex.'/is', $url[$i], $matches); + + if(count($matches)) { + $parameterValue = $matches[0]; + } + } + + // Add parameter value + $parameters[$parameter] = $parameterValue; + } + } + + // This route matches + if($matches) { + $this->parameters = $parameters; + return $this; + } + } + } + + // No match here, move on... + return null; + } + + /** + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * @param string $url + * @return self + */ + public function setUrl($url) { + + $parameters = $this->parseParameters($url); + + if($parameters !== null) { + foreach($parameters as $param) { + $this->parameters[$param] = ''; + } + } + + $this->url = $url; + return $this; + } + + /** + * @param array $aliases + * @return self + */ + public function setAliases(array $aliases) { + $this->aliases = $aliases; + return $this; + } + + /** + * Add alias + * + * @param $alias + * @return self + */ + public function addAlias($alias) { + $this->aliases[] = $alias; + return $this; + } + + public function getAliases() { + $this->aliases; + } + + /** + * Add request type + * + * @param $type + * @return self + * @throws RouterException + */ + public function addRequestType($type) { + if(!in_array($type, self::$allowedRequestTypes)) { + throw new RouterException('Invalid request method: ' . $type); + } + + $this->requestTypes[] = $type; + return $this; + } + + /** + * @return mixed + */ + public function getRequestTypes() { + return $this->requestTypes; + } +} \ No newline at end of file diff --git a/src/Pecee/Router.php b/src/Pecee/SimpleRouter/SimpleRouter.php similarity index 56% rename from src/Pecee/Router.php rename to src/Pecee/SimpleRouter/SimpleRouter.php index 0755832..e99671d 100644 --- a/src/Pecee/Router.php +++ b/src/Pecee/SimpleRouter/SimpleRouter.php @@ -7,17 +7,21 @@ * This class is added so calls can be made staticly like Router::get() making the code look more pretty. */ -namespace Pecee; +namespace Pecee\SimpleRouter; -use Pecee\Router\SimpleRouter; +class SimpleRouter { -class Router { + public static function start($defaultNamespace = null) { + $router = RouterBase::GetInstance(); + $router->setDefaultNamespace($defaultNamespace); + $router->routeRequest(); + } public static function get($url, $callback) { $route = new RouterRoute($url, $callback); $route->addRequestType(RouterRoute::REQUEST_TYPE_GET); - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($route); return $route; @@ -27,7 +31,7 @@ class Router { $route = new RouterRoute($url, $callback); $route->addRequestType(RouterRoute::REQUEST_TYPE_POST); - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($route); return $route; @@ -37,7 +41,7 @@ class Router { $route = new RouterRoute($url, $callback); $route->addRequestType(RouterRoute::REQUEST_TYPE_PUT); - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($route); return $route; @@ -47,7 +51,7 @@ class Router { $route = new RouterRoute($url, $callback); $route->addRequestType(RouterRoute::REQUEST_TYPE_DELETE); - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($route); return $route; @@ -61,7 +65,7 @@ class Router { $group->setSettings($settings); } - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($group); return $group; @@ -73,10 +77,38 @@ class Router { $route->addRequestType($requestType); } - $router = SimpleRouter::GetInstance(); + $router = RouterBase::getInstance(); $router->addRoute($route); return $route; } + public static function all($url, $callback) { + $route = new RouterRoute($url, $callback); + $router = RouterBase::getInstance(); + $router->addRoute($route); + + return $route; + } + + public static function controller($url, $controller) { + $route = new RouterController($url, $controller); + $router = RouterBase::getInstance(); + $router->addRoute($route); + + return $route; + } + + public static function ressource($url, $controller) { + $route = new RouterRessource($url, $controller); + $router = RouterBase::getInstance(); + $router->addRoute($route); + + return $route; + } + + public function getRoute($controller = null, $parameters = null, $getParams = null) { + return RouterBase::getInstance()->getRoute($controller, $parameters, $getParams); + } + } \ No newline at end of file