diff --git a/AltoRouter.php b/AltoRouter.php index 7bc9178..fa9e451 100644 --- a/AltoRouter.php +++ b/AltoRouter.php @@ -11,277 +11,287 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -class AltoRouter { +class AltoRouter +{ - /** - * @var array Array of all routes (incl. named routes). - */ - protected $routes = array(); + /** + * @var array Array of all routes (incl. named routes). + */ + protected $routes = []; - /** - * @var array Array of all named routes. - */ - protected $namedRoutes = array(); + /** + * @var array Array of all named routes. + */ + protected $namedRoutes = []; - /** - * @var string Can be used to ignore leading part of the Request URL (if main file lives in subdirectory of host) - */ - protected $basePath = ''; + /** + * @var string Can be used to ignore leading part of the Request URL (if main file lives in subdirectory of host) + */ + protected $basePath = ''; - /** - * @var array Array of default match types (regex helpers) - */ - protected $matchTypes = array( - 'i' => '[0-9]++', - 'a' => '[0-9A-Za-z]++', - 'h' => '[0-9A-Fa-f]++', - '*' => '.+?', - '**' => '.++', - '' => '[^/\.]++' - ); + /** + * @var array Array of default match types (regex helpers) + */ + protected $matchTypes = [ + 'i' => '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++' + ]; - /** - * Create router in one call from config. - * - * @param array $routes - * @param string $basePath - * @param array $matchTypes - */ - public function __construct( $routes = array(), $basePath = '', $matchTypes = array() ) { - $this->addRoutes($routes); - $this->setBasePath($basePath); - $this->addMatchTypes($matchTypes); - } - - /** - * Retrieves all routes. - * Useful if you want to process or display routes. - * @return array All routes. - */ - public function getRoutes() { - return $this->routes; - } + /** + * Create router in one call from config. + * + * @param array $routes + * @param string $basePath + * @param array $matchTypes + * @throws Exception + */ + public function __construct(array $routes = [], $basePath = '', $matchTypes = []) + { + $this->addRoutes($routes); + $this->setBasePath($basePath); + $this->addMatchTypes($matchTypes); + } + + /** + * Retrieves all routes. + * Useful if you want to process or display routes. + * @return array All routes. + */ + public function getRoutes() + { + return $this->routes; + } - /** - * Add multiple routes at once from array in the following format: - * - * $routes = array( - * array($method, $route, $target, $name) - * ); - * - * @param array $routes - * @return void - * @author Koen Punt - * @throws Exception - */ - public function addRoutes($routes){ - if(!is_array($routes) && !$routes instanceof Traversable) { - throw new \Exception('Routes should be an array or an instance of Traversable'); - } - foreach($routes as $route) { - call_user_func_array(array($this, 'map'), $route); - } - } + /** + * Add multiple routes at once from array in the following format: + * + * $routes = [ + * [$method, $route, $target, $name] + * ]; + * + * @param array $routes + * @return void + * @author Koen Punt + * @throws Exception + */ + public function addRoutes($routes) + { + if (!is_array($routes) && !$routes instanceof Traversable) { + throw new RuntimeException('Routes should be an array or an instance of Traversable'); + } + foreach ($routes as $route) { + call_user_func_array([$this, 'map'], $route); + } + } - /** - * Set the base path. - * Useful if you are running your application from a subdirectory. - */ - public function setBasePath($basePath) { - $this->basePath = $basePath; - } + /** + * Set the base path. + * Useful if you are running your application from a subdirectory. + * @param string $basePath + */ + public function setBasePath($basePath) + { + $this->basePath = $basePath; + } - /** - * Add named match types. It uses array_merge so keys can be overwritten. - * - * @param array $matchTypes The key is the name and the value is the regex. - */ - public function addMatchTypes($matchTypes) { - $this->matchTypes = array_merge($this->matchTypes, $matchTypes); - } + /** + * Add named match types. It uses array_merge so keys can be overwritten. + * + * @param array $matchTypes The key is the name and the value is the regex. + */ + public function addMatchTypes(array $matchTypes) + { + $this->matchTypes = array_merge($this->matchTypes, $matchTypes); + } - /** - * Map a route to a target - * - * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) - * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] - * @param mixed $target The target where this route should point to. Can be anything. - * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. - * @throws Exception - */ - public function map($method, $route, $target, $name = null) { + /** + * Map a route to a target + * + * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) + * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] + * @param mixed $target The target where this route should point to. Can be anything. + * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. + * @throws Exception + */ + public function map($method, $route, $target, $name = null) + { - $this->routes[] = array($method, $route, $target, $name); + $this->routes[] = [$method, $route, $target, $name]; - if($name) { - if(isset($this->namedRoutes[$name])) { - throw new \Exception("Can not redeclare route '{$name}'"); - } else { - $this->namedRoutes[$name] = $route; - } + if ($name) { + if (isset($this->namedRoutes[$name])) { + throw new RuntimeException("Can not redeclare route '{$name}'"); + } + $this->namedRoutes[$name] = $route; + } - } + return; + } - return; - } + /** + * Reversed routing + * + * Generate the URL for a named route. Replace regexes with supplied parameters + * + * @param string $routeName The name of the route. + * @param array @params Associative array of parameters to replace placeholders with. + * @return string The URL of the route with named parameters in place. + * @throws Exception + */ + public function generate($routeName, array $params = []) + { - /** - * Reversed routing - * - * Generate the URL for a named route. Replace regexes with supplied parameters - * - * @param string $routeName The name of the route. - * @param array @params Associative array of parameters to replace placeholders with. - * @return string The URL of the route with named parameters in place. - * @throws Exception - */ - public function generate($routeName, array $params = array()) { + // Check if named route exists + if (!isset($this->namedRoutes[$routeName])) { + throw new RuntimeException("Route '{$routeName}' does not exist."); + } - // Check if named route exists - if(!isset($this->namedRoutes[$routeName])) { - throw new \Exception("Route '{$routeName}' does not exist."); - } + // Replace named parameters + $route = $this->namedRoutes[$routeName]; + + // prepend base path to route url again + $url = $this->basePath . $route; - // Replace named parameters - $route = $this->namedRoutes[$routeName]; - - // prepend base path to route url again - $url = $this->basePath . $route; + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + foreach ($matches as $index => $match) { + list($block, $pre, $type, $param, $optional) = $match; - if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + if ($pre) { + $block = substr($block, 1); + } - foreach($matches as $index => $match) { - list($block, $pre, $type, $param, $optional) = $match; + if (isset($params[$param])) { + // Part is found, replace for param value + $url = str_replace($block, $params[$param], $url); + } elseif ($optional && $index !== 0) { + // Only strip preceding slash if it's not at the base + $url = str_replace($pre . $block, '', $url); + } else { + // Strip match block + $url = str_replace($block, '', $url); + } + } + } - if ($pre) { - $block = substr($block, 1); - } + return $url; + } - if(isset($params[$param])) { - // Part is found, replace for param value - $url = str_replace($block, $params[$param], $url); - } elseif ($optional && $index !== 0) { - // Only strip preceeding slash if it's not at the base - $url = str_replace($pre . $block, '', $url); - } else { - // Strip match block - $url = str_replace($block, '', $url); - } - } + /** + * Match a given Request Url against stored routes + * @param string $requestUrl + * @param string $requestMethod + * @return array|boolean Array with route information on success, false on failure (no match). + */ + public function match($requestUrl = null, $requestMethod = null) + { - } + $params = []; - return $url; - } + // set Request Url if it isn't passed as parameter + if ($requestUrl === null) { + $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + } - /** - * Match a given Request Url against stored routes - * @param string $requestUrl - * @param string $requestMethod - * @return array|boolean Array with route information on success, false on failure (no match). - */ - public function match($requestUrl = null, $requestMethod = null) { + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); - $params = array(); - $match = false; + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } - // set Request Url if it isn't passed as parameter - if($requestUrl === null) { - $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; - } + // set Request Method if it isn't passed as a parameter + if ($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } - // strip base path from request url - $requestUrl = substr($requestUrl, strlen($this->basePath)); + foreach ($this->routes as $handler) { + list($methods, $route, $target, $name) = $handler; - // Strip query string (?a=b) from Request Url - if (($strpos = strpos($requestUrl, '?')) !== false) { - $requestUrl = substr($requestUrl, 0, $strpos); - } + $method_match = (stripos($methods, $requestMethod) !== false); - // set Request Method if it isn't passed as a parameter - if($requestMethod === null) { - $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; - } + // Method did not match, continue to next route. + if (!$method_match) { + continue; + } - foreach($this->routes as $handler) { - list($methods, $route, $target, $name) = $handler; + if ($route === '*') { + // * wildcard (matches all) + $match = true; + } elseif (isset($route[0]) && $route[0] === '@') { + // @ regex delimiter + $pattern = '`' . substr($route, 1) . '`u'; + $match = preg_match($pattern, $requestUrl, $params) === 1; + } elseif (($position = strpos($route, '[')) === false) { + // No params in url, do string comparison + $match = strcmp($requestUrl, $route) === 0; + } else { + // Compare longest non-param string with url + if (strncmp($requestUrl, $route, $position) !== 0) { + continue; + } + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params) === 1; + } - $method_match = (stripos($methods, $requestMethod) !== false); + if ($match) { + if ($params) { + foreach ($params as $key => $value) { + if (is_numeric($key)) { + unset($params[$key]); + } + } + } - // Method did not match, continue to next route. - if (!$method_match) continue; + return [ + 'target' => $target, + 'params' => $params, + 'name' => $name + ]; + } + } + return false; + } - if ($route === '*') { - // * wildcard (matches all) - $match = true; - } elseif (isset($route[0]) && $route[0] === '@') { - // @ regex delimiter - $pattern = '`' . substr($route, 1) . '`u'; - $match = preg_match($pattern, $requestUrl, $params) === 1; - } elseif (($position = strpos($route, '[')) === false) { - // No params in url, do string comparison - $match = strcmp($requestUrl, $route) === 0; - } else { - // Compare longest non-param string with url - if (strncmp($requestUrl, $route, $position) !== 0) { - continue; - } - $regex = $this->compileRoute($route); - $match = preg_match($regex, $requestUrl, $params) === 1; - } + /** + * Compile the regex for a given route (EXPENSIVE) + * @param $route + * @return string + */ + protected function compileRoute($route) + { + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + $matchTypes = $this->matchTypes; + foreach ($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; - if ($match) { + if (isset($matchTypes[$type])) { + $type = $matchTypes[$type]; + } + if ($pre === '.') { + $pre = '\.'; + } - if ($params) { - foreach($params as $key => $value) { - if(is_numeric($key)) unset($params[$key]); - } - } + $optional = $optional !== '' ? '?' : null; + + //Older versions of PCRE require the 'P' in (?P) + $pattern = '(?:' + . ($pre !== '' ? $pre : null) + . '(' + . ($param !== '' ? "?P<$param>" : null) + . $type + . ')' + . $optional + . ')' + . $optional; - return array( - 'target' => $target, - 'params' => $params, - 'name' => $name - ); - } - } - return false; - } - - /** - * Compile the regex for a given route (EXPENSIVE) - */ - protected function compileRoute($route) { - if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { - - $matchTypes = $this->matchTypes; - foreach($matches as $match) { - list($block, $pre, $type, $param, $optional) = $match; - - if (isset($matchTypes[$type])) { - $type = $matchTypes[$type]; - } - if ($pre === '.') { - $pre = '\.'; - } - - $optional = $optional !== '' ? '?' : null; - - //Older versions of PCRE require the 'P' in (?P) - $pattern = '(?:' - . ($pre !== '' ? $pre : null) - . '(' - . ($param !== '' ? "?P<$param>" : null) - . $type - . ')' - . $optional - . ')' - . $optional; - - $route = str_replace($block, $pattern, $route); - } - - } - return "`^$route$`u"; - } + $route = str_replace($block, $pattern, $route); + } + } + return "`^$route$`u"; + } } diff --git a/composer.json b/composer.json index 28affa1..285aeaf 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ }, "require-dev": { "phpunit/phpunit": "5.7.*", - "codeclimate/php-test-reporter": "dev-master" + "codeclimate/php-test-reporter": "dev-master", + "squizlabs/php_codesniffer": "3.4.2" }, "autoload": { "classmap": ["AltoRouter.php"] diff --git a/examples/basic/index.php b/examples/basic/index.php index a31239c..f5c9484 100644 --- a/examples/basic/index.php +++ b/examples/basic/index.php @@ -12,10 +12,10 @@ if (file_exists($_SERVER['SCRIPT_FILENAME']) && pathinfo($_SERVER['SCRIPT_FILENA $router = new AltoRouter(); $router->setBasePath('/AltoRouter/examples/basic'); -$router->map('GET|POST','/', 'home#index', 'home'); -$router->map('GET','/users/', array('c' => 'UserController', 'a' => 'ListAction')); -$router->map('GET','/users/[i:id]', 'users#show', 'users_show'); -$router->map('POST','/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do'); +$router->map('GET|POST', '/', 'home#index', 'home'); +$router->map('GET', '/users/', ['c' => 'UserController', 'a' => 'ListAction']); +$router->map('GET', '/users/[i:id]', 'users#show', 'users_show'); +$router->map('POST', '/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do'); // match current request $match = $router->match(); @@ -24,12 +24,12 @@ $match = $router->match();

Current request:

-	Target: 
-	Params: 
-	Name: 	
+    Target: 
+    Params: 
+    Name:   
 

Try these requests:

GET generate('home'); ?>

-

GET generate('users_show', array('id' => 5)); ?>

-

+

GET generate('users_show', ['id' => 5]); ?>

+

diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..d59cccf --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,10 @@ + + + rules + + + tests + AltoRouter.php + examples/ + + diff --git a/tests/AltoRouterTest.php b/tests/AltoRouterTest.php index b609940..5a85986 100644 --- a/tests/AltoRouterTest.php +++ b/tests/AltoRouterTest.php @@ -2,519 +2,573 @@ require 'AltoRouter.php'; -class AltoRouterDebug extends AltoRouter{ - public function getNamedRoutes(){ - return $this->namedRoutes; - } - - public function getBasePath(){ - return $this->basePath; - } +class AltoRouterDebug extends AltoRouter +{ + public function getNamedRoutes() + { + return $this->namedRoutes; + } + public function getBasePath() + { + return $this->basePath; + } } -class SimpleTraversable implements Iterator{ +class SimpleTraversable implements Iterator +{ - protected $_position = 0; + protected $_position = 0; - protected $_data = array( - array('GET', '/foo', 'foo_action', null), - array('POST', '/bar', 'bar_action', 'second_route') - ); - - public function current(){ - return $this->_data[$this->_position]; - } - public function key(){ - return $this->_position; - } - public function next(){ - ++$this->_position; - } - public function rewind(){ - $this->_position = 0; - } - public function valid(){ - return isset($this->_data[$this->_position]); - } + protected $_data = [ + ['GET', '/foo', 'foo_action', null], + ['POST', '/bar', 'bar_action', 'second_route'] + ]; + public function current() + { + return $this->_data[$this->_position]; + } + public function key() + { + return $this->_position; + } + public function next() + { + ++$this->_position; + } + public function rewind() + { + $this->_position = 0; + } + public function valid() + { + return isset($this->_data[$this->_position]); + } } class AltoRouterTest extends PHPUnit\Framework\TestCase { - /** - * @var AltoRouter - */ - protected $router; + /** + * @var AltoRouter + */ + protected $router; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - $this->router = new AltoRouterDebug; - } + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + $this->router = new AltoRouterDebug; + } - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - { - } + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + { + } - /** - * @covers AltoRouter::getRoutes - */ - public function testGetRoutes() - { - $method = 'POST'; - $route = '/[:controller]/[:action]'; - $target = function(){}; + /** + * @covers AltoRouter::getRoutes + */ + public function testGetRoutes() + { + $method = 'POST'; + $route = '/[:controller]/[:action]'; + $target = static function () { + }; - $this->assertInternalType('array', $this->router->getRoutes()); - // $this->assertIsArray($this->router->getRoutes()); // for phpunit 7.x - $this->router->map($method, $route, $target); - $this->assertEquals(array(array($method, $route, $target, null)), $this->router->getRoutes()); - } + $this->assertInternalType('array', $this->router->getRoutes()); + // $this->assertIsArray($this->router->getRoutes()); // for phpunit 7.x + $this->router->map($method, $route, $target); + $this->assertEquals([[$method, $route, $target, null]], $this->router->getRoutes()); + } - /** - * @covers AltoRouter::addRoutes - */ - public function testAddRoutes() - { - $method = 'POST'; - $route = '/[:controller]/[:action]'; - $target = function(){}; - - $this->router->addRoutes(array( - array($method, $route, $target), - array($method, $route, $target, 'second_route') - )); - - $routes = $this->router->getRoutes(); - - $this->assertEquals(array($method, $route, $target, null), $routes[0]); - $this->assertEquals(array($method, $route, $target, 'second_route'), $routes[1]); - } + /** + * @covers AltoRouter::addRoutes + */ + public function testAddRoutes() + { + $method = 'POST'; + $route = '/[:controller]/[:action]'; + $target = static function () { + }; + + $this->router->addRoutes([ + [$method, $route, $target], + [$method, $route, $target, 'second_route'] + ]); + + $routes = $this->router->getRoutes(); + + $this->assertEquals([$method, $route, $target, null], $routes[0]); + $this->assertEquals([$method, $route, $target, 'second_route'], $routes[1]); + } - /** - * @covers AltoRouter::addRoutes - */ - public function testAddRoutesAcceptsTraverable() - { - $traversable = new SimpleTraversable(); - $this->router->addRoutes($traversable); - - $traversable->rewind(); - - $first = $traversable->current(); - $traversable->next(); - $second = $traversable->current(); - - $routes = $this->router->getRoutes(); - - $this->assertEquals($first, $routes[0]); - $this->assertEquals($second, $routes[1]); - } + /** + * @covers AltoRouter::addRoutes + */ + public function testAddRoutesAcceptsTraverable() + { + $traversable = new SimpleTraversable(); + $this->router->addRoutes($traversable); + + $traversable->rewind(); + + $first = $traversable->current(); + $traversable->next(); + $second = $traversable->current(); + + $routes = $this->router->getRoutes(); + + $this->assertEquals($first, $routes[0]); + $this->assertEquals($second, $routes[1]); + } - /** - * @covers AltoRouter::addRoutes - * @expectedException Exception - */ - public function testAddRoutesThrowsExceptionOnInvalidArgument() - { - $this->router->addRoutes(new stdClass); - } + /** + * @covers AltoRouter::addRoutes + * @expectedException Exception + */ + public function testAddRoutesThrowsExceptionOnInvalidArgument() + { + $this->router->addRoutes(new stdClass); + } - /** - * @covers AltoRouter::setBasePath - */ - public function testSetBasePath() - { - $basePath = $this->router->setBasePath('/some/path'); - $this->assertEquals('/some/path', $this->router->getBasePath()); - - $basePath = $this->router->setBasePath('/some/path'); - $this->assertEquals('/some/path', $this->router->getBasePath()); - } + /** + * @covers AltoRouter::setBasePath + */ + public function testSetBasePath() + { + $basePath = $this->router->setBasePath('/some/path'); + $this->assertEquals('/some/path', $this->router->getBasePath()); + + $basePath = $this->router->setBasePath('/some/path'); + $this->assertEquals('/some/path', $this->router->getBasePath()); + } - /** - * @covers AltoRouter::map - */ - public function testMap() - { - $method = 'POST'; - $route = '/[:controller]/[:action]'; - $target = function(){}; - - $this->router->map($method, $route, $target); - - $routes = $this->router->getRoutes(); - - $this->assertEquals(array($method, $route, $target, null), $routes[0]); - } + /** + * @covers AltoRouter::map + */ + public function testMap() + { + $method = 'POST'; + $route = '/[:controller]/[:action]'; + $target = static function () { + }; + + $this->router->map($method, $route, $target); + + $routes = $this->router->getRoutes(); + + $this->assertEquals([$method, $route, $target, null], $routes[0]); + } - /** - * @covers AltoRouter::map - */ - public function testMapWithName() - { - $method = 'POST'; - $route = '/[:controller]/[:action]'; - $target = function(){}; - $name = 'myroute'; - - $this->router->map($method, $route, $target, $name); - - $routes = $this->router->getRoutes(); - $this->assertEquals(array($method, $route, $target, $name), $routes[0]); - - $named_routes = $this->router->getNamedRoutes(); - $this->assertEquals($route, $named_routes[$name]); - - try{ - $this->router->map($method, $route, $target, $name); - $this->fail('Should not be able to add existing named route'); - }catch(Exception $e){ - $this->assertEquals("Can not redeclare route '{$name}'", $e->getMessage()); - } - } + /** + * @covers AltoRouter::map + */ + public function testMapWithName() + { + $method = 'POST'; + $route = '/[:controller]/[:action]'; + $target = static function () { + }; + $name = 'myroute'; + + $this->router->map($method, $route, $target, $name); + + $routes = $this->router->getRoutes(); + $this->assertEquals([$method, $route, $target, $name], $routes[0]); + + $named_routes = $this->router->getNamedRoutes(); + $this->assertEquals($route, $named_routes[$name]); + + try { + $this->router->map($method, $route, $target, $name); + $this->fail('Should not be able to add existing named route'); + } catch (Exception $e) { + $this->assertEquals("Can not redeclare route '{$name}'", $e->getMessage()); + } + } - /** - * @covers AltoRouter::generate - */ - public function testGenerate() - { - $params = array( - 'controller' => 'test', - 'action' => 'someaction' - ); - - $this->router->map('GET', '/[:controller]/[:action]', function(){}, 'foo_route'); - - $this->assertEquals('/test/someaction', - $this->router->generate('foo_route', $params)); - - $params = array( - 'controller' => 'test', - 'action' => 'someaction', - 'type' => 'json' - ); - - $this->assertEquals('/test/someaction', - $this->router->generate('foo_route', $params)); - - } + /** + * @covers AltoRouter::generate + */ + public function testGenerate() + { + $params =[ + 'controller' => 'test', + 'action' => 'someaction' + ]; + + $this->router->map('GET', '/[:controller]/[:action]', static function () { + }, 'foo_route'); + + $this->assertEquals( + '/test/someaction', + $this->router->generate('foo_route', $params) + ); + + $params = [ + 'controller' => 'test', + 'action' => 'someaction', + 'type' => 'json' + ]; + + $this->assertEquals( + '/test/someaction', + $this->router->generate('foo_route', $params) + ); + } - public function testGenerateWithOptionalUrlParts() - { - $this->router->map('GET', '/[:controller]/[:action].[:type]?', function(){}, 'bar_route'); - - $params = array( - 'controller' => 'test', - 'action' => 'someaction' - ); - - $this->assertEquals('/test/someaction', - $this->router->generate('bar_route', $params)); - - $params = array( - 'controller' => 'test', - 'action' => 'someaction', - 'type' => 'json' - ); - - $this->assertEquals('/test/someaction.json', - $this->router->generate('bar_route', $params)); - } + /** + * @covers AltoRouter::generate + */ + public function testGenerateWithOptionalUrlParts() + { + $this->router->map('GET', '/[:controller]/[:action].[:type]?', static function () { + }, 'bar_route'); + + $params = [ + 'controller' => 'test', + 'action' => 'someaction' + ]; + + $this->assertEquals( + '/test/someaction', + $this->router->generate('bar_route', $params) + ); + + $params = [ + 'controller' => 'test', + 'action' => 'someaction', + 'type' => 'json' + ]; + + $this->assertEquals( + '/test/someaction.json', + $this->router->generate('bar_route', $params) + ); + } - /** - * GitHub #98 - */ - public function testGenerateWithOptionalPartOnBareUrl() - { - $this->router->map('GET', '/[i:page]?', function(){}, 'bare_route'); - - $params = array( - 'page' => 1 - ); - - $this->assertEquals('/1', - $this->router->generate('bare_route', $params)); - - $params = array(); - - $this->assertEquals('/', - $this->router->generate('bare_route', $params)); - } + /** + * GitHub #98 + * @covers AltoRouter::generate + */ + public function testGenerateWithOptionalPartOnBareUrl() + { + $this->router->map('GET', '/[i:page]?', static function () { + }, 'bare_route'); + + $params = [ + 'page' => 1 + ]; + + $this->assertEquals( + '/1', + $this->router->generate('bare_route', $params) + ); + + $params = []; + + $this->assertEquals( + '/', + $this->router->generate('bare_route', $params) + ); + } - public function testGenerateWithNonexistingRoute() - { - try{ - $this->router->generate('nonexisting_route'); - $this->fail('Should trigger an exception on nonexisting named route'); - }catch(Exception $e){ - $this->assertEquals("Route 'nonexisting_route' does not exist.", $e->getMessage()); - } - } - - /** - * @covers AltoRouter::match - * @covers AltoRouter::compileRoute - */ - public function testMatch() - { - $this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route'); - - $this->assertEquals(array( - 'target' => 'foo_action', - 'params' => array( - 'controller' => 'test', - 'action' => 'do' - ), - 'name' => 'foo_route' - ), $this->router->match('/foo/test/do', 'GET')); - - $this->assertFalse($this->router->match('/foo/test/do', 'POST')); - - $this->assertEquals(array( - 'target' => 'foo_action', - 'params' => array( - 'controller' => 'test', - 'action' => 'do' - ), - 'name' => 'foo_route' - ), $this->router->match('/foo/test/do?param=value', 'GET')); - - } + /** + * @covers AltoRouter::generate + */ + public function testGenerateWithNonexistingRoute() + { + try { + $this->router->generate('nonexisting_route'); + $this->fail('Should trigger an exception on nonexisting named route'); + } catch (Exception $e) { + $this->assertEquals("Route 'nonexisting_route' does not exist.", $e->getMessage()); + } + } + + /** + * @covers AltoRouter::match + * @covers AltoRouter::compileRoute + */ + public function testMatch() + { + $this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route'); + + $this->assertEquals([ + 'target' => 'foo_action', + 'params' => [ + 'controller' => 'test', + 'action' => 'do' + ], + 'name' => 'foo_route' + ], $this->router->match('/foo/test/do', 'GET')); + + $this->assertFalse($this->router->match('/foo/test/do', 'POST')); + + $this->assertEquals([ + 'target' => 'foo_action', + 'params' => [ + 'controller' => 'test', + 'action' => 'do' + ], + 'name' => 'foo_route' + ], $this->router->match('/foo/test/do?param=value', 'GET')); + } - public function testMatchWithNonRegex() { - $this->router->map('GET','/about-us', 'PagesController#about', 'about_us'); + /** + * @covers AltoRouter::match + */ + public function testMatchWithNonRegex() + { + $this->router->map('GET', '/about-us', 'PagesController#about', 'about_us'); - $this->assertEquals(array( - 'target' => 'PagesController#about', - 'params' => array(), - 'name' => 'about_us' - ), $this->router->match('/about-us', 'GET')); + $this->assertEquals([ + 'target' => 'PagesController#about', + 'params' => [], + 'name' => 'about_us' + ], $this->router->match('/about-us', 'GET')); - $this->assertFalse($this->router->match('/about-us', 'POST')); - $this->assertFalse($this->router->match('/about', 'GET')); - $this->assertFalse($this->router->match('/about-us-again', 'GET')); - } - - public function testMatchWithFixedParamValues() - { - $this->router->map('POST','/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do'); - - $this->assertEquals(array( - 'target' => 'usersController#doAction', - 'params' => array( - 'id' => 1, - 'action' => 'delete' - ), - 'name' => 'users_do' - ), $this->router->match('/users/1/delete', 'POST')); - - $this->assertFalse($this->router->match('/users/1/delete', 'GET')); - $this->assertFalse($this->router->match('/users/abc/delete', 'POST')); - $this->assertFalse($this->router->match('/users/1/create', 'GET')); - } - - public function testMatchWithPlainRoute() - { - $router = $this->getMockBuilder('AltoRouterDebug') - ->setMethods(array('compileRoute')) - ->getMock(); - - // this should prove that compileRoute is not called when the route doesn't - // have any params in it, but this doesn't work because compileRoute is private. - $router->expects($this->never()) - ->method('compileRoute'); + $this->assertFalse($this->router->match('/about-us', 'POST')); + $this->assertFalse($this->router->match('/about', 'GET')); + $this->assertFalse($this->router->match('/about-us-again', 'GET')); + } - $router->map('GET', '/contact', 'website#contact', 'contact'); + /** + * @covers AltoRouter::match + */ + public function testMatchWithFixedParamValues() + { + $this->router->map('POST', '/users/[i:id]/[delete|update:action]', 'usersController#doAction', 'users_do'); + + $this->assertEquals([ + 'target' => 'usersController#doAction', + 'params' => [ + 'id' => 1, + 'action' => 'delete' + ], + 'name' => 'users_do' + ], $this->router->match('/users/1/delete', 'POST')); + + $this->assertFalse($this->router->match('/users/1/delete', 'GET')); + $this->assertFalse($this->router->match('/users/abc/delete', 'POST')); + $this->assertFalse($this->router->match('/users/1/create', 'GET')); + } - // exact match, so no regex compilation necessary - $this->assertEquals(array( - 'target' => 'website#contact', - 'params' => array(), - 'name' => 'contact' - ), $router->match('/contact', 'GET')); + /** + * @covers AltoRouter::match + */ + public function testMatchWithPlainRoute() + { + $router = $this->getMockBuilder('AltoRouterDebug') + ->setMethods(['compileRoute']) + ->getMock(); + + // this should prove that compileRoute is not called when the route doesn't + // have any params in it, but this doesn't work because compileRoute is private. + $router->expects($this->never()) + ->method('compileRoute'); - $router->map('GET', '/page/[:id]', 'pages#show', 'page'); + $router->map('GET', '/contact', 'website#contact', 'contact'); - // no prefix match, so no regex compilation necessary - $this->assertFalse($router->match('/page1', 'GET')); + // exact match, so no regex compilation necessary + $this->assertEquals([ + 'target' => 'website#contact', + 'params' => [], + 'name' => 'contact' + ], $router->match('/contact', 'GET')); - } - - public function testMatchWithServerVars() - { - $this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route'); - - $_SERVER['REQUEST_URI'] = '/foo/test/do'; - $_SERVER['REQUEST_METHOD'] = 'GET'; - - $this->assertEquals(array( - 'target' => 'foo_action', - 'params' => array( - 'controller' => 'test', - 'action' => 'do' - ), - 'name' => 'foo_route' - ), $this->router->match()); - } - - public function testMatchWithOptionalUrlParts() - { - $this->router->map('GET', '/bar/[:controller]/[:action].[:type]?', 'bar_action', 'bar_route'); - - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array( - 'controller' => 'test', - 'action' => 'do', - 'type' => 'json' - ), - 'name' => 'bar_route' - ), $this->router->match('/bar/test/do.json', 'GET')); - - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array( - 'controller' => 'test', - 'action' => 'do' - ), - 'name' => 'bar_route' - ), $this->router->match('/bar/test/do', 'GET')); - } - - /** - * GitHub #98 - */ - public function testMatchWithOptionalPartOnBareUrl(){ - $this->router->map('GET', '/[i:page]?', 'bare_action', 'bare_route'); - - $this->assertEquals(array( - 'target' => 'bare_action', - 'params' => array( - 'page' => 1 - ), - 'name' => 'bare_route' - ), $this->router->match('/1', 'GET')); - - $this->assertEquals(array( - 'target' => 'bare_action', - 'params' => array(), - 'name' => 'bare_route' - ), $this->router->match('/', 'GET')); - } - - public function testMatchWithWildcard() - { - $this->router->map('GET', '/a', 'foo_action', 'foo_route'); - $this->router->map('GET', '*', 'bar_action', 'bar_route'); - - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array(), - 'name' => 'bar_route' - ), $this->router->match('/everything', 'GET')); - - } - - public function testMatchWithCustomRegexp() - { - $this->router->map('GET', '@^/[a-z]*$', 'bar_action', 'bar_route'); - - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array(), - 'name' => 'bar_route' - ), $this->router->match('/everything', 'GET')); - - $this->assertFalse($this->router->match('/some-other-thing', 'GET')); - - } + $router->map('GET', '/page/[:id]', 'pages#show', 'page'); - public function testMatchWithUnicodeRegex() - { - $pattern = '/(?[^'; - // Arabic characters - $pattern .= '\x{0600}-\x{06FF}'; - $pattern .= '\x{FB50}-\x{FDFD}'; - $pattern .= '\x{FE70}-\x{FEFF}'; - $pattern .= '\x{0750}-\x{077F}'; - // Alphanumeric, /, _, - and space characters - $pattern .= 'a-zA-Z0-9\/_\-\s'; - // 'ZERO WIDTH NON-JOINER' - $pattern .= '\x{200C}'; - $pattern .= ']+)'; - - $this->router->map('GET', '@' . $pattern, 'unicode_action', 'unicode_route'); - - $this->assertEquals(array( - 'target' => 'unicode_action', - 'name' => 'unicode_route', - 'params' => array( - 'path' => '大家好' - ) - ), $this->router->match('/大家好', 'GET')); - - $this->assertFalse($this->router->match('/﷽‎', 'GET')); - } + // no prefix match, so no regex compilation necessary + $this->assertFalse($router->match('/page1', 'GET')); + } - /** - * @covers AltoRouter::addMatchTypes - */ - public function testMatchWithCustomNamedRegex() - { - $this->router->addMatchTypes(array('cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?')); - $this->router->map('GET', '/bar/[cId:customId]', 'bar_action', 'bar_route'); - - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array( - 'customId' => 'AB1', - ), - 'name' => 'bar_route' - ), $this->router->match('/bar/AB1', 'GET')); + /** + * @covers AltoRouter::match + */ + public function testMatchWithServerVars() + { + $this->router->map('GET', '/foo/[:controller]/[:action]', 'foo_action', 'foo_route'); + + $_SERVER['REQUEST_URI'] = '/foo/test/do'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $this->assertEquals([ + 'target' => 'foo_action', + 'params' => [ + 'controller' => 'test', + 'action' => 'do' + ], + 'name' => 'foo_route' + ], $this->router->match()); + } - $this->assertEquals(array( - 'target' => 'bar_action', - 'params' => array( - 'customId' => 'AB1_0123456789', - ), - 'name' => 'bar_route' - ), $this->router->match('/bar/AB1_0123456789', 'GET')); - - $this->assertFalse($this->router->match('/some-other-thing', 'GET')); - - } + /** + * @covers AltoRouter::match + */ + public function testMatchWithOptionalUrlParts() + { + $this->router->map('GET', '/bar/[:controller]/[:action].[:type]?', 'bar_action', 'bar_route'); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [ + 'controller' => 'test', + 'action' => 'do', + 'type' => 'json' + ], + 'name' => 'bar_route' + ], $this->router->match('/bar/test/do.json', 'GET')); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [ + 'controller' => 'test', + 'action' => 'do' + ], + 'name' => 'bar_route' + ], $this->router->match('/bar/test/do', 'GET')); + } + + /** + * GitHub #98 + * @covers AltoRouter::match + */ + public function testMatchWithOptionalPartOnBareUrl() + { + $this->router->map('GET', '/[i:page]?', 'bare_action', 'bare_route'); + + $this->assertEquals([ + 'target' => 'bare_action', + 'params' => [ + 'page' => 1 + ], + 'name' => 'bare_route' + ], $this->router->match('/1', 'GET')); + + $this->assertEquals([ + 'target' => 'bare_action', + 'params' => [], + 'name' => 'bare_route' + ], $this->router->match('/', 'GET')); + } - public function testMatchWithCustomNamedUnicodeRegex() - { - $pattern = '[^'; - // Arabic characters - $pattern .= '\x{0600}-\x{06FF}'; - $pattern .= '\x{FB50}-\x{FDFD}'; - $pattern .= '\x{FE70}-\x{FEFF}'; - $pattern .= '\x{0750}-\x{077F}'; - $pattern .= ']+'; - - $this->router->addMatchTypes(array('nonArabic' => $pattern)); - $this->router->map('GET', '/bar/[nonArabic:string]', 'non_arabic_action', 'non_arabic_route'); - - $this->assertEquals(array( - 'target' => 'non_arabic_action', - 'name' => 'non_arabic_route', - 'params' => array( - 'string' => 'some-path' - ) - ), $this->router->match('/bar/some-path', 'GET')); - - $this->assertFalse($this->router->match('/﷽‎', 'GET')); - } + /** + * @covers AltoRouter::match + */ + public function testMatchWithWildcard() + { + $this->router->map('GET', '/a', 'foo_action', 'foo_route'); + $this->router->map('GET', '*', 'bar_action', 'bar_route'); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [], + 'name' => 'bar_route' + ], $this->router->match('/everything', 'GET')); + } + /** + * @covers AltoRouter::match + */ + public function testMatchWithCustomRegexp() + { + $this->router->map('GET', '@^/[a-z]*$', 'bar_action', 'bar_route'); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [], + 'name' => 'bar_route' + ], $this->router->match('/everything', 'GET')); + + $this->assertFalse($this->router->match('/some-other-thing', 'GET')); + } + /** + * @covers AltoRouter::match + */ + public function testMatchWithUnicodeRegex() + { + $pattern = '/(?[^'; + // Arabic characters + $pattern .= '\x{0600}-\x{06FF}'; + $pattern .= '\x{FB50}-\x{FDFD}'; + $pattern .= '\x{FE70}-\x{FEFF}'; + $pattern .= '\x{0750}-\x{077F}'; + // Alphanumeric, /, _, - and space characters + $pattern .= 'a-zA-Z0-9\/_\-\s'; + // 'ZERO WIDTH NON-JOINER' + $pattern .= '\x{200C}'; + $pattern .= ']+)'; + + $this->router->map('GET', '@' . $pattern, 'unicode_action', 'unicode_route'); + + $this->assertEquals([ + 'target' => 'unicode_action', + 'name' => 'unicode_route', + 'params' => [ + 'path' => '大家好' + ] + ], $this->router->match('/大家好', 'GET')); + + $this->assertFalse($this->router->match('/﷽‎', 'GET')); + } + + /** + * @covers AltoRouter::addMatchTypes + */ + public function testMatchWithCustomNamedRegex() + { + $this->router->addMatchTypes(['cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?']); + $this->router->map('GET', '/bar/[cId:customId]', 'bar_action', 'bar_route'); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [ + 'customId' => 'AB1', + ], + 'name' => 'bar_route' + ], $this->router->match('/bar/AB1', 'GET')); + + $this->assertEquals([ + 'target' => 'bar_action', + 'params' => [ + 'customId' => 'AB1_0123456789', + ], + 'name' => 'bar_route' + ], $this->router->match('/bar/AB1_0123456789', 'GET')); + + $this->assertFalse($this->router->match('/some-other-thing', 'GET')); + } + /** + * @covers AltoRouter::addMatchTypes + */ + public function testMatchWithCustomNamedUnicodeRegex() + { + $pattern = '[^'; + // Arabic characters + $pattern .= '\x{0600}-\x{06FF}'; + $pattern .= '\x{FB50}-\x{FDFD}'; + $pattern .= '\x{FE70}-\x{FEFF}'; + $pattern .= '\x{0750}-\x{077F}'; + $pattern .= ']+'; + + $this->router->addMatchTypes(['nonArabic' => $pattern]); + $this->router->map('GET', '/bar/[nonArabic:string]', 'non_arabic_action', 'non_arabic_route'); + + $this->assertEquals([ + 'target' => 'non_arabic_action', + 'name' => 'non_arabic_route', + 'params' => [ + 'string' => 'some-path' + ] + ], $this->router->match('/bar/some-path', 'GET')); + + $this->assertFalse($this->router->match('/﷽‎', 'GET')); + } } diff --git a/tests/benchmark.php b/tests/benchmark.php index 99b2584..ff469e9 100644 --- a/tests/benchmark.php +++ b/tests/benchmark.php @@ -14,10 +14,11 @@ require __DIR__ . '/../vendor/autoload.php'; global $argv; -$n = isset( $argv[1] ) ? intval( $argv[1] ) : 1000; +$n = isset($argv[1]) ? intval($argv[1]) : 1000; // generates a random request url -function random_request_url() { +function random_request_url() +{ $characters = 'abcdefghijklmnopqrstuvwxyz'; $charactersLength = strlen($characters); $randomString = '/'; @@ -26,45 +27,46 @@ function random_request_url() { for ($i = 0; $i < rand(5, 20); $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; - if( rand(1, 10) === 1 ) { - $randomString .= '/'; + if (rand(1, 10) === 1) { + $randomString .= '/'; } } // add dynamic route with 10% chance - if ( rand(1, 10) === 1 ) { - $randomString = rtrim( $randomString, '/' ) . '/[:part]'; + if (rand(1, 10) === 1) { + $randomString = rtrim($randomString, '/') . '/[:part]'; } return $randomString; } // generate a random request method -function random_request_method() { - static $methods = array( 'GET', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ); - $random_key = array_rand( $methods ); +function random_request_method() +{ + static $methods = [ 'GET', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ]; + $random_key = array_rand($methods); return $methods[ $random_key ]; } // prepare benchmark data -$requests = array(); -for($i=0; $i<$n; $i++) { - $requests[] = array( +$requests = []; +for ($i=0; $i<$n; $i++) { + $requests[] = [ 'method' => random_request_method(), 'url' => random_request_url(), - ); + ]; } $router = new AltoRouter(); // map requests $start = microtime(true); -foreach($requests as $r) { +foreach ($requests as $r) { $router->map($r['method'], $r['url'], ''); } $end = microtime(true); $map_time = ($end - $start) * 1000; -echo sprintf( 'Map time: %.2f ms', $map_time ) . PHP_EOL; +echo sprintf('Map time: %.2f ms', $map_time) . PHP_EOL; // pick random route to match @@ -75,19 +77,16 @@ $start = microtime(true); $router->match($r['url'], $r['method']); $end = microtime(true); $match_time_known_route = ($end - $start) * 1000; -echo sprintf( 'Match time (known route): %.2f ms', $match_time_known_route ) . PHP_EOL; +echo sprintf('Match time (known route): %.2f ms', $match_time_known_route) . PHP_EOL; // match unexisting route $start = microtime(true); $router->match('/55-foo-bar', 'GET'); $end = microtime(true); $match_time_unknown_route = ($end - $start) * 1000; -echo sprintf( 'Match time (unknown route): %.2f ms', $match_time_unknown_route ) . PHP_EOL; +echo sprintf('Match time (unknown route): %.2f ms', $match_time_unknown_route) . PHP_EOL; // print totals echo sprintf('Total time: %.2f seconds', ($map_time + $match_time_known_route + $match_time_unknown_route)) . PHP_EOL; -echo sprintf('Memory usage: %d KB', round( memory_get_usage() / 1024 )) . PHP_EOL; -echo sprintf('Peak memory usage: %d KB', round( memory_get_peak_usage( true ) / 1024 )) . PHP_EOL; - - - +echo sprintf('Memory usage: %d KB', round(memory_get_usage() / 1024)) . PHP_EOL; +echo sprintf('Peak memory usage: %d KB', round(memory_get_peak_usage(true) / 1024)) . PHP_EOL;