From 55acac68fa258ea555e8fc1e35534b2c260a2ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Sessing=C3=B8?= Date: Fri, 18 Sep 2015 18:38:59 +0200 Subject: [PATCH] [TASK] Initial release --- README.md | 92 +++++++++++- src/Pecee/Router.php | 82 +++++++++++ src/Pecee/Router/RouterAlias.php | 6 + src/Pecee/Router/RouterEntry.php | 202 +++++++++++++++++++++++++++ src/Pecee/Router/RouterException.php | 3 + src/Pecee/Router/RouterGroup.php | 21 +++ src/Pecee/Router/RouterRoute.php | 106 ++++++++++++++ src/Pecee/Router/SimpleRouter.php | 149 ++++++++++++++++++++ 8 files changed, 659 insertions(+), 2 deletions(-) create mode 100644 src/Pecee/Router.php create mode 100644 src/Pecee/Router/RouterAlias.php create mode 100644 src/Pecee/Router/RouterEntry.php create mode 100644 src/Pecee/Router/RouterException.php create mode 100644 src/Pecee/Router/RouterGroup.php create mode 100644 src/Pecee/Router/RouterRoute.php create mode 100644 src/Pecee/Router/SimpleRouter.php diff --git a/README.md b/README.md index b45dd9b..44239cc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# simple-php-router -Simple PHP router, easy to load in through composer - heavily inspired by Laravel. +# Simple PHP router +Simple, fast PHP router - easy to load in through composer and heavily inspired by Laravel. + +## Installation +Add the latest version pf Simple PHP Router to your ```composer.json``` + +```json +{ + "require": { + "pecee/simple-php-router": "1.*" + }, + "require-dev": { + "pecee/simple-php-router": "1.*" + } +} +``` + +## 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. + +This is an example of a basic ```index.php``` file: + +```php +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() +``` + +## 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; + +/* + * 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. + * / + +Router::group(['prefix' => 'v1', 'middleware' => '\MyWebsite\Middleware\SomeMiddlewareClass'], function() { + + Router::group(['prefix' => 'services'], function() { + + Router::get('/answers/{id}', 'ControllerAnswers@show'); + + }); +}); +``` + +### 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. + +```php +use \Pecee\SimpleRouter; +use \Pecee\Router\RouterRoute; + +$router = SimpleRouter::GetInstance(); + +$route = new RouterRoute('/answer/1', function() { + die('this callback will match /answer/1'); +}); + +$route->setMiddleware('\HSWebserviceV1\Middleware\AuthMiddleware'); +$route->setNamespace('MyWebsite'); +$route->setPrefix('v1'); + +// Add the route to the router +$router->addRoute($route); +``` + +## Documentation +While I work on a better documentation, please refer to the Laravel 5 routing documentation here: + +http://laravel.com/docs/5.1/routing + +## Easily extendable +The router can be easily extended to customize your needs. \ No newline at end of file diff --git a/src/Pecee/Router.php b/src/Pecee/Router.php new file mode 100644 index 0000000..0755832 --- /dev/null +++ b/src/Pecee/Router.php @@ -0,0 +1,82 @@ +addRequestType(RouterRoute::REQUEST_TYPE_GET); + + $router = SimpleRouter::GetInstance(); + $router->addRoute($route); + + return $route; + } + + public static function post($url, $callback) { + $route = new RouterRoute($url, $callback); + $route->addRequestType(RouterRoute::REQUEST_TYPE_POST); + + $router = SimpleRouter::GetInstance(); + $router->addRoute($route); + + return $route; + } + + public static function put($url, $callback) { + $route = new RouterRoute($url, $callback); + $route->addRequestType(RouterRoute::REQUEST_TYPE_PUT); + + $router = SimpleRouter::GetInstance(); + $router->addRoute($route); + + return $route; + } + + public static function delete($url, $callback) { + $route = new RouterRoute($url, $callback); + $route->addRequestType(RouterRoute::REQUEST_TYPE_DELETE); + + $router = SimpleRouter::GetInstance(); + $router->addRoute($route); + + return $route; + } + + public static function group($settings = array(), $callback) { + $group = new RouterGroup(); + $group->setCallback($callback); + + if($settings !== null && is_array($settings)) { + $group->setSettings($settings); + } + + $router = SimpleRouter::GetInstance(); + $router->addRoute($group); + + return $group; + } + + public static function match(array $requestTypes, $url, $callback) { + $route = new RouterRoute($url, $callback); + foreach($requestTypes as $requestType) { + $route->addRequestType($requestType); + } + + $router = SimpleRouter::GetInstance(); + $router->addRoute($route); + + return $route; + } + +} \ No newline at end of file diff --git a/src/Pecee/Router/RouterAlias.php b/src/Pecee/Router/RouterAlias.php new file mode 100644 index 0000000..c073dda --- /dev/null +++ b/src/Pecee/Router/RouterAlias.php @@ -0,0 +1,6 @@ +settings = array(); + $this->requestTypes = array(); + $this->parameters = 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]; + } + + return null; + } + + /** + * @param string $callback + * @return self; + */ + public function setCallback($callback) { + $this->callback = $callback; + return $this; + } + + /** + * @return mixed + */ + public function getCallback() { + 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, '/'); + return $this; + } + + /** + * @param string $middleware + * @return self + */ + public function setMiddleware($middleware) { + $this->middleware = $middleware; + return $this; + } + + /** + * @param string $namespace + * @return self + */ + public function setNamespace($namespace) { + $this->namespace = $namespace; + return $this; + } + + /** + * @return string + */ + public function getPrefix() { + return $this->prefix; + } + + /** + * @return string + */ + public function getMiddleware() { + return $this->middleware; + } + + /** + * @return string + */ + public function getNamespace() { + return $this->namespace; + } + + /** + * @return array + */ + public function getSettings() { + return $this->settings; + } + + /** + * Get settings that are allowed to be inherited by child routes. + * + * @return array + */ + public function getMergeableSettings() { + $settings = $this->settings; + + if(isset($settings['middleware'])) { + unset($settings['middleware']); + } + + if(isset($settings['prefix'])) { + unset($settings['prefix']); + } + + return $settings; + } + + /** + * @param array $settings + * @return self + */ + public function setSettings($settings) { + $this->settings = $settings; + return $this; + } + + /** + * Dynamicially access settings value + * + * @param $name + * @return mixed|null + */ + public function __get($name) { + return (isset($this->settings[$name]) ? $this->settings[$name] : null); + } + + /** + * Dynamicially set settings value + * + * @param string $name + * @param mixed|null $value + */ + public function __set($name, $value = null) { + $this->settings[$name] = $value; + } + + abstract function getRoute($requestMethod, &$url); + +} \ No newline at end of file diff --git a/src/Pecee/Router/RouterException.php b/src/Pecee/Router/RouterException.php new file mode 100644 index 0000000..815d0ab --- /dev/null +++ b/src/Pecee/Router/RouterException.php @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..c3bd5ea --- /dev/null +++ b/src/Pecee/Router/RouterRoute.php @@ -0,0 +1,106 @@ +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 new file mode 100644 index 0000000..328a222 --- /dev/null +++ b/src/Pecee/Router/SimpleRouter.php @@ -0,0 +1,149 @@ +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