1
0
mirror of https://github.com/dannyvankooten/AltoRouter.git synced 2025-08-20 07:11:19 +02:00

94 Commits

Author SHA1 Message Date
Danny van Kooten
c28df65f42 adhere to (mostly) PSR12, check codestyle in CI 2025-01-11 20:21:41 +01:00
Danny van Kooten
f440bb654d check php syntax with error_reporting=-1 2025-01-11 20:13:29 +01:00
Danny van Kooten
9931b97642 fix indentation 2025-01-05 21:33:28 +01:00
Danny van Kooten
41fcec6b8e Merge branch 'master' of github.com:dannyvankooten/AltoRouter 2025-01-05 21:30:50 +01:00
Danny van Kooten
9d62094f74 check php syntax on all supported versions 2025-01-05 21:30:35 +01:00
Danny van Kooten
bc6b911a6d Merge pull request #285 from xpetter/patch-1
Update AltoRouter.php for php 8.4
2024-12-16 14:27:40 +01:00
Petter
5159909a81 Update AltoRouter.php
fix for php 8.4 and keep php 7.3 support
2024-12-16 14:13:20 +01:00
Petter
71389237e5 Update AltoRouter.php for php 8.4
Fix for php8.4
AltoRouter::map(): Implicitly marking parameter $name as nullable is deprecated.
AltoRouter::match(): Implicitly marking parameter $requestUrl as nullable is deprecated.
AltoRouter::match(): Implicitly marking parameter $requestMethod as nullable is deprecated.
2024-12-16 10:00:57 +01:00
Danny van Kooten
36f67da100 bump required php version to 7.3 2023-11-30 10:10:31 +01:00
Danny van Kooten
4270bb5ca2 bump php version requirement to 7.0 and add types on all methods 2023-10-09 20:54:04 +02:00
Danny van Kooten
77a2e14681 fix phpcs issues 2023-10-09 20:49:37 +02:00
Danny van Kooten
f327fbb5bf test on php 8.2 and php 8.3 too 2023-10-09 20:46:07 +02:00
Danny van Kooten
63bb784d76 update links to point to docs 2023-10-09 20:23:45 +02:00
Danny van Kooten
bb7b009331 update phpunit to 9.6 & migrate config to latest schema 2023-10-09 19:46:04 +02:00
Danny van Kooten
20674b8953 address PHP8.x warnings about return type
this requires PHP 7.1 for the void return type, but only for running the
test suite.
2022-12-08 11:12:36 +01:00
Danny van Kooten
ac028a7f3f run tests only on php >= 7.3 2022-02-17 10:42:10 +01:00
Danny van Kooten
0a94ba4142 switch to GitHub actions for running test suite 2022-02-17 10:39:54 +01:00
Danny van Kooten
85c453b12c update phpunit to latest version 2022-02-17 10:31:41 +01:00
Danny van Kooten
f6fede4f94 Merge pull request #247 from aliazizi/patch-1
Check for empty request URI, thank you @aliazizi
2020-03-09 09:34:59 +01:00
Ali Akbar Azizi
4efac02fad Update AltoRouter.php
Check $request is empty string or not
2020-02-22 15:57:56 +03:30
Danny van Kooten
6ffb025022 use svg travis icon 2019-11-23 12:03:36 +01:00
Danny van Kooten
127f6e9699 fix #241 2019-11-23 12:01:41 +01:00
Danny van Kooten
a80bb36f11 add test for #241 2019-11-23 11:35:40 +01:00
Danny van Kooten
a51c74793a benchmark using real-life api for a better comparison 2019-11-10 21:16:42 +01:00
Danny van Kooten
7e2b48db00 type-hint array in constructor parameter too 2019-11-10 20:33:12 +01:00
Danny van Kooten
3a97b2c432 use short array notation in readme too 2019-11-10 20:32:52 +01:00
Danny van Kooten
98e97407de use vendored phpunit in travis 2019-11-10 20:27:38 +01:00
Danny van Kooten
17d00ed90e update readme 2019-11-10 20:26:36 +01:00
Danny van Kooten
a34caaf60c Merge pull request #234 from DevFelixDorn/code-maintainability
Add PHPCS & type-hinting for array function arguments. Use (CS enforced) short array notation everywhere. Thanks @DevFelixDorn
2019-11-10 20:18:39 +01:00
Félix Dorn
a01166760c phpcs formatting 2019-07-03 21:44:06 +02:00
Félix Dorn
cd918de64b add @covers on every test, remove array long syntax 2019-07-03 21:43:47 +02:00
Félix Dorn
2360f527b0 add type hinting, removed array long syntax 2019-07-03 21:40:17 +02:00
Félix Dorn
24360d6162 add php-cs to match with PSR2 requirements 2019-07-03 21:39:35 +02:00
Danny van Kooten
dce6efdea2 fix phpunit on older php versions 2019-02-07 09:52:34 +01:00
Danny van Kooten
ecb5f69042 use travis phpunit so that we can test on lower php versions too 2019-02-07 09:44:18 +01:00
Danny van Kooten
9e9767e8fa add support for php's built-in web server to example. #226 2019-02-07 09:38:22 +01:00
Danny van Kooten
87d93b6840 update benchmark 2019-02-07 09:16:23 +01:00
Danny van Kooten
9bdfc55eb3 fix tests by making compileRoute protected instead 2019-02-07 09:16:13 +01:00
Danny van Kooten
df96d7270c update to phpunit 7.5 2019-02-06 20:13:21 +01:00
Danny van Kooten
e2a07ec452 test recent php versions, stop testing 5.3-5.5 2019-02-06 20:01:07 +01:00
Danny van Kooten
ca2d425bc6 add example for url generation to readme 2018-08-19 11:51:01 +02:00
Danny van Kooten
281c5631db add license to main php file too 2018-08-19 11:47:59 +02:00
Danny van Kooten
58299f4d0d add license file 2018-08-19 11:47:09 +02:00
Danny van Kooten
1ff4165b28 improve benchmark by preparing first, matching & only mapping two routes 2017-04-10 16:36:22 +02:00
Koen Punt
689670f4dc Merge pull request #99 from koenpunt/bare-optional-parameter
Allow for optional parameters on bare/root url
2017-04-04 20:37:25 +02:00
Koen Punt
72dd5199bd multi optional 2017-04-02 20:41:58 +02:00
Koen Punt
be64536dcb prevent strip of preceding slash at base 2017-04-02 20:41:10 +02:00
Koen Punt
f18a57d481 add failing test for generate 2016-12-30 00:11:18 +01:00
Koen Punt
e72248c744 add failing test for #98 2016-12-30 00:11:18 +01:00
Koen Punt
693752b77a simplify loop for non-param substring (#131)
optimize comparison using strcmp functions
2016-06-21 23:27:25 +02:00
Danny van Kooten
a8ee8e875f Merge pull request #130 from dannyvankooten/strpos-request-method
Use `stripos` to compare request method (instead of `explode` + `in_array`) for improved performance.
2016-06-21 22:37:56 +02:00
Danny van Kooten
48524b67cb Add benchmark script to tests directory for easy benchmarking. 2016-06-21 15:27:21 +02:00
Danny van Kooten
39c5009247 Merge pull request #136 from koenpunt/travis-docker
Optimize Travis builds, thanks @koenpunt!
2015-11-30 07:47:43 +07:00
Koen Punt
018771bee3 add php 7.0 2015-11-29 16:30:06 +01:00
Koen Punt
13f227e843 add composer to travis cache 2015-11-29 16:26:36 +01:00
Koen Punt
d5f3643888 disable sudo to use docker builds 2015-11-29 16:10:24 +01:00
Danny van Kooten
6fe6f4b196 Use stripos to compare request method, instead of explode + in_array. 2015-11-03 08:13:51 +07:00
Danny van Kooten
b0d115431e Add tests for non-regex routes, see #100 2015-11-03 08:03:34 +07:00
Danny van Kooten
97b2ba7daf Add PHPDocs for class properties. 2015-11-03 07:37:53 +07:00
Danny van Kooten
c3f7a669a8 Add @throws declaration to method that can throw an Exception. 2015-11-03 07:37:36 +07:00
Danny van Kooten
c9ce952af1 Remove unused alteration of $_REQUEST superglobal. Closes #104 2015-11-03 07:37:04 +07:00
Danny van Kooten
e7c5afefca Add .gitattributes to ignore certain files & dirs when preparing export for things as Composer. (http://www.reddit.com/r/PHP/comments/2jzp6k/i_dont_need_your_tests_in_my_production/) 2015-03-28 11:51:38 +01:00
Koen Punt
f0e9c913a6 blacklist vendor 2015-03-02 16:51:27 +01:00
Koen Punt
7b2e59a444 Merge pull request #107 from dannyvankooten/phpunit-config
Use local phpunit
2015-03-02 15:41:30 +01:00
Koen Punt
2035dab8be rename phpunit.xml to phpunit.xml.dist 2015-03-02 15:13:56 +01:00
Koen Punt
88af3a6fa5 remove path to individual test 2015-03-02 15:10:38 +01:00
Koen Punt
ed1c3edfbd Use local phpunit
Also, no need to specify the path to the config
2015-03-02 15:10:03 +01:00
Danny van Kooten
0e1e2b7d63 Add CodeClimate test coverage reporter to Travis. Refactor tests into /tests subdirectory. 2015-03-02 19:35:56 +07:00
Danny van Kooten
0c01df1128 Add Test Coverage badge to README 2015-03-02 19:28:32 +07:00
Koen Punt
179d2fe5bc Merge pull request #105 from koenpunt/add-getroutes
add test for getRoutes
2015-03-01 20:53:39 +01:00
Koen Punt
3a932e7588 add test for getRoutes
remove obsolete getRoutes method from debug class
2015-03-01 20:47:54 +01:00
Danny van Kooten
60cf81c963 Merge pull request #103 from koenpunt/phpunit-composer
add phpunit to composer.json
2015-03-01 23:09:36 +07:00
Koen Punt
9bf911515c add phpunit to composer.json 2015-03-01 17:07:47 +01:00
Danny van Kooten
fa50148d7b Merge pull request #97 from SebastianPoell/patch-1
Update AltoRouter.php
2015-03-01 22:35:53 +07:00
Danny van Kooten
24d42506ec Fix badges in README 2015-03-01 22:32:15 +07:00
Danny van Kooten
6a77802419 Add badges to README 2015-03-01 22:29:40 +07:00
Danny van Kooten
f4b09c3b6c Add CodeClimate to README. Update MIT copyright year. 2015-03-01 22:27:17 +07:00
Sebastian Pöll
40adf80fd2 Update AltoRouter.php 2014-12-19 16:20:11 +01:00
Sebastian Pöll
3de8ea70d5 Update AltoRouter.php
Added @return to getRoutes. Removed unnecessary whitespaces.
2014-12-19 16:13:03 +01:00
Sebastian Pöll
009f163c3d Update AltoRouter.php
Added function getRoutes(). Often it's nice to process routes or display them without extending AltoRouter.
2014-12-19 15:26:51 +01:00
Danny van Kooten
a7a6b99ac8 Merge branch 'master' of github.com:dannyvankooten/AltoRouter 2014-10-07 18:13:18 +02:00
Danny van Kooten
384d0ed35b Readme update, link usage sections to AltoRouter.com 2014-10-07 18:11:22 +02:00
Koen Punt
4acd26880c Fix incorrect example 2014-10-07 10:22:05 +02:00
Danny van Kooten
c0041d10e7 Merge pull request #79 from dannyvankooten/cleanup-readme
Update/Cleanup README
2014-10-07 09:24:30 +02:00
John Long
f468fe9c02 Merge pull request #88 from Nyholm/patch-1
Added PHP 5.6 and HHVM to travis.yml
2014-09-25 06:41:42 -05:00
Tobias Nyholm
fdadb3119d Added PHP 5.6 and HHVM to travis.yml 2014-09-25 08:21:10 +02:00
Koen Punt
768d3ea445 mention addRoutes 2014-05-10 17:52:49 +02:00
Koen Punt
4b1c205de4 Cleanup README 2014-05-10 17:44:34 +02:00
Koen Punt
cd145f993e Merge pull request #78 from koenpunt/escape-dash
escape dash to fix tests
2014-05-10 14:58:06 +02:00
Koen Punt
99f75c28e6 escape dash to fix tests 2014-05-10 14:56:15 +02:00
Danny van Kooten
aa0706284d Merge pull request #75 from dannyvankooten/add-patch-method-type
Add PATCH to comment as valid method.
2014-05-08 11:16:37 +02:00
Koen Punt
55da8fcbda Add PATCH to comment as valid method.
In frameworks like Rails the PATCH method is preferred over PUT. More on this: http://weblog.rubyonrails.org/2012/2/25/edge-rails-patch-is-the-new-primary-http-method-for-updates/
2014-05-07 13:39:15 +02:00
Danny van Kooten
353596ec4f Merge pull request #66 from MathB/master
Add matching documentation
2014-04-17 13:25:07 +02:00
MathB
5cf275f1b5 Add matching documentation 2014-04-08 23:14:24 +02:00
15 changed files with 1118 additions and 733 deletions

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/examples export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
/Gemfile export-ignore
/Gemfile.lock export-ignore

20
.github/workflows/php-check-syntax.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Check PHP syntax
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', 'highest']
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- name: checkout repo
uses: actions/checkout@v3
- run: composer run check-syntax

42
.github/workflows/php.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: PHP
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: [ '7.3', 'highest' ]
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: composer
- name: Validate composer.json and composer.lock
run: composer validate
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --no-progress
- name: Check codestyle
run: composer run-script check-codestyle
- name: Run test suite
run: composer run-script test

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.idea
_site
Gemfile
Gemfile.lock
build
vendor
composer.lock
phpunit.xml
.phpunit.result.cache

View File

@@ -1,7 +0,0 @@
language: php
php:
- 5.3
- 5.4
- 5.5
script: phpunit --coverage-text ./

View File

@@ -1,270 +1,300 @@
<?php
class AltoRouter {
/*
MIT License
protected $routes = array();
protected $namedRoutes = array();
protected $basePath = '';
protected $matchTypes = array(
'i' => '[0-9]++',
'a' => '[0-9A-Za-z]++',
'h' => '[0-9A-Fa-f]++',
'*' => '.+?',
'**' => '.++',
'' => '[^/\.]++'
);
Copyright (c) 2012 Danny van Kooten <hi@dannyvankooten.com>
/**
* 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);
}
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
/**
* 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
*/
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);
}
}
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
/**
* Set the base path.
* Useful if you are running your application from a subdirectory.
*/
public function setBasePath($basePath) {
$this->basePath = $basePath;
}
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.
*/
/**
* 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);
}
class AltoRouter
{
/**
* @var array Array of all routes (incl. named routes).
*/
protected $routes = [];
/**
* Map a route to a target
*
* @param string $method One of 4 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|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.
*/
public function map($method, $route, $target, $name = null) {
/**
* @var array Array of all named routes.
*/
protected $namedRoutes = [];
$this->routes[] = array($method, $route, $target, $name);
/**
* @var string Can be used to ignore leading part of the Request URL (if main file lives in subdirectory of host)
*/
protected $basePath = '';
if($name) {
if(isset($this->namedRoutes[$name])) {
throw new \Exception("Can not redeclare route '{$name}'");
} else {
$this->namedRoutes[$name] = $route;
}
/**
* @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
* @throws Exception
*/
public function __construct(array $routes = [], string $basePath = '', array $matchTypes = [])
{
$this->addRoutes($routes);
$this->setBasePath($basePath);
$this->addMatchTypes($matchTypes);
}
return;
}
/**
* Retrieves all routes.
* Useful if you want to process or display routes.
* @return array All routes.
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* 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.
*/
public function generate($routeName, array $params = array()) {
/**
* 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);
}
}
// Check if named route exists
if(!isset($this->namedRoutes[$routeName])) {
throw new \Exception("Route '{$routeName}' does not exist.");
}
/**
* Set the base path.
* Useful if you are running your application from a subdirectory.
* @param string $basePath
*/
public function setBasePath(string $basePath)
{
$this->basePath = $basePath;
}
// Replace named parameters
$route = $this->namedRoutes[$routeName];
// prepend base path to route url again
$url = $this->basePath . $route;
/**
* 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);
}
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
/**
* 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(string $method, string $route, $target, ?string $name = null)
{
foreach($matches as $match) {
list($block, $pre, $type, $param, $optional) = $match;
$this->routes[] = [$method, $route, $target, $name];
if ($pre) {
$block = substr($block, 1);
}
if ($name) {
if (isset($this->namedRoutes[$name])) {
throw new RuntimeException("Can not redeclare route '{$name}'");
}
$this->namedRoutes[$name] = $route;
}
}
if(isset($params[$param])) {
$url = str_replace($block, $params[$param], $url);
} elseif ($optional) {
$url = str_replace($pre . $block, '', $url);
}
}
/**
* 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(string $routeName, array $params = []): string
{
// Check if named route exists
if (!isset($this->namedRoutes[$routeName])) {
throw new RuntimeException("Route '{$routeName}' does not exist.");
}
}
// Replace named parameters
$route = $this->namedRoutes[$routeName];
return $url;
}
// prepend base path to route url again
$url = $this->basePath . $route;
/**
* 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) {
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
foreach ($matches as $index => $match) {
list($block, $pre, $type, $param, $optional) = $match;
$params = array();
$match = false;
if ($pre) {
$block = substr($block, 1);
}
// set Request Url if it isn't passed as parameter
if($requestUrl === null) {
$requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
}
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);
}
}
}
// strip base path from request url
$requestUrl = substr($requestUrl, strlen($this->basePath));
return $url;
}
// Strip query string (?a=b) from Request Url
if (($strpos = strpos($requestUrl, '?')) !== false) {
$requestUrl = substr($requestUrl, 0, $strpos);
}
/**
* 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(?string $requestUrl = null, ?string $requestMethod = null)
{
// set Request Method if it isn't passed as a parameter
if($requestMethod === null) {
$requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
}
$params = [];
// Force request_order to be GP
// http://www.mail-archive.com/internals@lists.php.net/msg33119.html
$_REQUEST = array_merge($_GET, $_POST);
// set Request Url if it isn't passed as parameter
if ($requestUrl === null) {
$requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
}
foreach($this->routes as $handler) {
list($method, $_route, $target, $name) = $handler;
// strip base path from request url
$requestUrl = substr($requestUrl, strlen($this->basePath));
$methods = explode('|', $method);
$method_match = false;
// Strip query string (?a=b) from Request Url
if (($strpos = strpos($requestUrl, '?')) !== false) {
$requestUrl = substr($requestUrl, 0, $strpos);
}
// Check if request method matches. If not, abandon early. (CHEAP)
foreach($methods as $method) {
if (strcasecmp($requestMethod, $method) === 0) {
$method_match = true;
break;
}
}
$lastRequestUrlChar = $requestUrl ? $requestUrl[strlen($requestUrl) - 1] : '';
// Method did not match, continue to next route.
if(!$method_match) continue;
// set Request Method if it isn't passed as a parameter
if ($requestMethod === null) {
$requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
}
// Check for a wildcard (matches all)
if ($_route === '*') {
$match = true;
} elseif (isset($_route[0]) && $_route[0] === '@') {
$match = preg_match('`' . substr($_route, 1) . '`u', $requestUrl, $params);
} else {
$route = null;
$regex = false;
$j = 0;
$n = isset($_route[0]) ? $_route[0] : null;
$i = 0;
foreach ($this->routes as $handler) {
list($methods, $route, $target, $name) = $handler;
// Find the longest non-regex substring and match it against the URI
while (true) {
if (!isset($_route[$i])) {
break;
} elseif (false === $regex) {
$c = $n;
$regex = $c === '[' || $c === '(' || $c === '.';
if (false === $regex && false !== isset($_route[$i+1])) {
$n = $_route[$i + 1];
$regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
}
if (false === $regex && $c !== '/' && (!isset($requestUrl[$j]) || $c !== $requestUrl[$j])) {
continue 2;
}
$j++;
}
$route .= $_route[$i++];
}
$method_match = (stripos($methods, $requestMethod) !== false);
$regex = $this->compileRoute($route);
$match = preg_match($regex, $requestUrl, $params);
}
// Method did not match, continue to next route.
if (!$method_match) {
continue;
}
if(($match == true || $match > 0)) {
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 before moving on to regex
// Check if last character before param is a slash, because it could be optional if param is optional too (see https://github.com/dannyvankooten/AltoRouter/issues/241)
if (strncmp($requestUrl, $route, $position) !== 0 && ($lastRequestUrlChar === '/' || $route[$position - 1] !== '/')) {
continue;
}
if($params) {
foreach($params as $key => $value) {
if(is_numeric($key)) unset($params[$key]);
}
}
$regex = $this->compileRoute($route);
$match = preg_match($regex, $requestUrl, $params) === 1;
}
return array(
'target' => $target,
'params' => $params,
'name' => $name
);
}
}
return false;
}
if ($match) {
if ($params) {
foreach ($params as $key => $value) {
if (is_numeric($key)) {
unset($params[$key]);
}
}
}
/**
* Compile the regex for a given route (EXPENSIVE)
*/
private function compileRoute($route) {
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
return [
'target' => $target,
'params' => $params,
'name' => $name
];
}
}
$matchTypes = $this->matchTypes;
foreach($matches as $match) {
list($block, $pre, $type, $param, $optional) = $match;
return false;
}
if (isset($matchTypes[$type])) {
$type = $matchTypes[$type];
}
if ($pre === '.') {
$pre = '\.';
}
/**
* Compile the regex for a given route (EXPENSIVE)
* @param string $route
* @return string
*/
protected function compileRoute(string $route): string
{
if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) {
$matchTypes = $this->matchTypes;
foreach ($matches as $match) {
list($block, $pre, $type, $param, $optional) = $match;
//Older versions of PCRE require the 'P' in (?P<named>)
$pattern = '(?:'
. ($pre !== '' ? $pre : null)
. '('
. ($param !== '' ? "?P<$param>" : null)
. $type
. '))'
. ($optional !== '' ? '?' : null);
if (isset($matchTypes[$type])) {
$type = $matchTypes[$type];
}
if ($pre === '.') {
$pre = '\.';
}
$route = str_replace($block, $pattern, $route);
}
$optional = $optional !== '' ? '?' : null;
}
return "`^$route$`u";
}
//Older versions of PCRE require the 'P' in (?P<named>)
$pattern = '(?:'
. ($pre !== '' ? $pre : null)
. '('
. ($param !== '' ? "?P<$param>" : null)
. $type
. ')'
. $optional
. ')'
. $optional;
$route = str_replace($block, $pattern, $route);
}
}
return "`^$route$`u";
}
}

View File

@@ -1,423 +0,0 @@
<?php
require 'AltoRouter.php';
class AltoRouterDebug extends AltoRouter{
public function getNamedRoutes(){
return $this->namedRoutes;
}
public function getRoutes(){
return $this->routes;
}
public function getBasePath(){
return $this->basePath;
}
}
class SimpleTraversable implements Iterator{
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]);
}
}
/**
* Generated by PHPUnit_SkeletonGenerator 1.2.1 on 2013-07-14 at 17:47:46.
*/
class AltoRouterTest extends PHPUnit_Framework_TestCase
{
/**
* @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;
}
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown()
{
}
/**
* @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 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::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 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::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));
}
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));
}
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'));
}
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 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'));
}
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'));
}
public function testMatchWithUnicodeRegex()
{
$pattern = '/(?<path>[^';
// 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'));
}
/**
* @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'));
$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'));
}
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'));
}
}

9
LICENSE.md Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2012 Danny van Kooten <hi@dannyvankooten.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

101
README.md
View File

@@ -1,77 +1,42 @@
# AltoRouter [![Build Status](https://api.travis-ci.org/dannyvankooten/AltoRouter.png)](http://travis-ci.org/dannyvankooten/AltoRouter)
AltoRouter is a small but powerful routing class for PHP 5.3+, heavily inspired by [klein.php](https://github.com/chriso/klein.php/).
# AltoRouter ![PHP status](https://github.com/dannyvankooten/AltoRouter/workflows/PHP/badge.svg) [![Latest Stable Version](https://poser.pugx.org/altorouter/altorouter/v/stable.svg)](https://packagist.org/packages/altorouter/altorouter) [![License](https://poser.pugx.org/altorouter/altorouter/license.svg)](https://packagist.org/packages/altorouter/altorouter)
* Dynamic routing with named parameters
AltoRouter is a small but powerful routing class, heavily inspired by [klein.php](https://github.com/chriso/klein.php/).
```php
$router = new AltoRouter();
// map homepage
$router->map('GET', '/', function() {
require __DIR__ . '/views/home.php';
});
// dynamic named route
$router->map('GET|POST', '/users/[i:id]/', function($id) {
$user = .....
require __DIR__ . '/views/user/details.php';
}, 'user-details');
// echo URL to user-details page for ID 5
echo $router->generate('user-details', ['id' => 5]); // Output: "/users/5"
```
## Features
* Can be used with all HTTP Methods
* Dynamic routing with named route parameters
* Reversed routing
* Flexible regular expression routing (inspired by [Sinatra](http://www.sinatrarb.com/))
* Custom regexes
## Getting started
1. PHP 5.3.x is required
2. Install AltoRouter using Composer or manually
2. Setup URL rewriting so that all requests are handled by **index.php**
3. Create an instance of AltoRouter, map your routes and match a request.
4. Have a look at the basic example in the `examples` directory for a better understanding on how to use AltoRouter.
## Routing
```php
$router = new AltoRouter();
$router->setBasePath('/AltoRouter'); // (optional) the subdir AltoRouter lives in
// mapping routes
$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');
// reversed routing
$router->generate('users_show', array('id' => 5));
```
**You can use the following limits on your named parameters. AltoRouter will create the correct regexes for you.**
```php
* // Match all request URIs
[i] // Match an integer
[i:id] // Match an integer as 'id'
[a:action] // Match alphanumeric characters as 'action'
[h:key] // Match hexadecimal characters as 'key'
[:action] // Match anything up to the next / or end of the URI as 'action'
[create|edit:action] // Match either 'create' or 'edit' as 'action'
[*] // Catch all (lazy, stops at the next trailing slash)
[*:trailing] // Catch all as 'trailing' (lazy)
[**:trailing] // Catch all (possessive - will match the rest of the URI)
.[:format]? // Match an optional parameter 'format' - a / or . before the block is also optional
```
**Some more complicated examples**
```php
@/(?[A-Za-z]{2}_[A-Za-z]{2})$ // custom regex, matches language codes like "en_us" etc.
/posts/[*:title][i:id] // Matches "/posts/this-is-a-title-123"
/output.[xml|json:format]? // Matches "/output", "output.xml", "output.json"
/[:controller]?/[:action]? // Matches the typical /controller/action format
```
**The character before the colon (the 'match type') is a shortcut for one of the following regular expressions**
```php
'i' => '[0-9]++'
'a' => '[0-9A-Za-z]++'
'h' => '[0-9A-Fa-f]++'
'*' => '.+?'
'**' => '.++'
'' => '[^/\.]++'
```
**New match types can be added using the `addMatchTypes()` method**
```php
$router->addMatchTypes(array('cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?'));
```
You need PHP >= 7.3 to use AltoRouter, although we highly recommend you [use an officially supported PHP version](https://secure.php.net/supported-versions.php) that is not EOL.
- [Install AltoRouter](https://dannyvankooten.github.io/AltoRouter//usage/install.html)
- [Rewrite all requests to AltoRouter](https://dannyvankooten.github.io/AltoRouter//usage/rewrite-requests.html)
- [Map your routes](https://dannyvankooten.github.io/AltoRouter//usage/mapping-routes.html)
- [Match requests](https://dannyvankooten.github.io/AltoRouter//usage/matching-requests.html)
- [Process the request your preferred way](https://dannyvankooten.github.io/AltoRouter//usage/processing-requests.html)
## Contributors
- [Danny van Kooten](https://github.com/dannyvankooten)
@@ -81,9 +46,9 @@ $router->addMatchTypes(array('cId' => '[a-zA-Z]{2}[0-9](?:_[0-9]++)?'));
## License
(MIT License)
MIT License
Copyright (c) 2012-2013 Danny van Kooten <hi@dannyvankooten.com>
Copyright (c) 2012 Danny van Kooten <hi@dannyvankooten.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -20,9 +20,18 @@
}
],
"require": {
"php": ">=5.3.0"
"php": ">=7.3"
},
"require-dev": {
"phpunit/phpunit": "9.6.*",
"squizlabs/php_codesniffer": "3.6.2"
},
"autoload": {
"classmap": ["AltoRouter.php"]
},
"scripts": {
"test": "phpunit",
"check-syntax": "find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php --define error_reporting=-1 -l",
"check-codestyle": "phpcs -ns"
}
}

View File

@@ -1,13 +1,21 @@
<?php
require '../../AltoRouter.php';
require __DIR__ . '/../../AltoRouter.php';
/**
* This can be useful if you're using PHP's built-in web server, to serve files like images or css
* @link https://secure.php.net/manual/en/features.commandline.webserver.php
*/
if (file_exists($_SERVER['SCRIPT_FILENAME']) && pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_EXTENSION) !== 'php') {
return;
}
$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();
@@ -16,12 +24,12 @@ $match = $router->match();
<h3>Current request: </h3>
<pre>
Target: <?php var_dump($match['target']); ?>
Params: <?php var_dump($match['params']); ?>
Name: <?php var_dump($match['name']); ?>
Target: <?php var_dump($match['target']); ?>
Params: <?php var_dump($match['params']); ?>
Name: <?php var_dump($match['name']); ?>
</pre>
<h3>Try these requests: </h3>
<p><a href="<?php echo $router->generate('home'); ?>">GET <?php echo $router->generate('home'); ?></a></p>
<p><a href="<?php echo $router->generate('users_show', array('id' => 5)); ?>">GET <?php echo $router->generate('users_show', array('id' => 5)); ?></a></p>
<p><form action="<?php echo $router->generate('users_do', array('id' => 10, 'action' => 'update')); ?>" method="post"><button type="submit"><?php echo $router->generate('users_do', array('id' => 10, 'action' => 'update')); ?></button></form></p>
<p><a href="<?php echo $router->generate('users_show', ['id' => 5]); ?>">GET <?php echo $router->generate('users_show', ['id' => 5]); ?></a></p>
<p><form action="<?php echo $router->generate('users_do', ['id' => 10, 'action' => 'update']); ?>" method="post"><button type="submit"><?php echo $router->generate('users_do', ['id' => 10, 'action' => 'update']); ?></button></form></p>

11
phpcs.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<ruleset name="rules">
<description>rules</description>
<rule ref="PSR12">
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
</rule>
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<file>AltoRouter.php</file>
<file>examples/</file>
<arg name="colors"/>
</ruleset>

17
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" verbose="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<file>./AltoRouter.php</file>
</include>
<report>
<clover outputFile="build/logs/clover.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="altorouter">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<logging/>
</phpunit>

581
tests/AltoRouterTest.php Normal file
View File

@@ -0,0 +1,581 @@
<?php
require 'AltoRouter.php';
class AltoRouterDebug extends AltoRouter
{
public function getNamedRoutes()
{
return $this->namedRoutes;
}
public function getBasePath()
{
return $this->basePath;
}
}
class SimpleTraversable implements Iterator
{
protected $_position = 0;
protected $_data = [
['GET', '/foo', 'foo_action', null],
['POST', '/bar', 'bar_action', 'second_route']
];
#[\ReturnTypeWillChange]
public function current()
{
return $this->_data[$this->_position];
}
#[\ReturnTypeWillChange]
public function key()
{
return $this->_position;
}
public function next() : void
{
++$this->_position;
}
public function rewind() : void
{
$this->_position = 0;
}
public function valid() : bool
{
return isset($this->_data[$this->_position]);
}
}
class AltoRouterTest extends PHPUnit\Framework\TestCase
{
/**
* @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() : void
{
$this->router = new AltoRouterDebug;
}
/**
* @covers AltoRouter::getRoutes
*/
public function testGetRoutes()
{
$method = 'POST';
$route = '/[:controller]/[:action]';
$target = static function () {
};
$this->assertIsArray($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 = 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
* @expectedException Exception
*/
public function testAddRoutesThrowsExceptionOnInvalidArgument()
{
$this->expectException(RuntimeException::class);
$this->router->addRoutes(new stdClass);
}
/**
* @covers AltoRouter::setBasePath
*/
public function testSetBasePath()
{
$this->router->setBasePath('/some/path');
$this->assertEquals('/some/path', $this->router->getBasePath());
$this->router->setBasePath('/some/path');
$this->assertEquals('/some/path', $this->router->getBasePath());
}
/**
* @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 = 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 =[
'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)
);
}
/**
* @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
* @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)
);
}
/**
* @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'));
}
/**
* @covers AltoRouter::match
*/
public function testMatchWithNonRegex()
{
$this->router->map('GET', '/about-us', 'PagesController#about', 'about_us');
$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'));
}
/**
* @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'));
}
/**
* @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
$router->expects($this->never())
->method('compileRoute');
$router->map('GET', '/contact', 'website#contact', 'contact');
// exact match, so no regex compilation necessary
$this->assertEquals([
'target' => 'website#contact',
'params' => [],
'name' => 'contact'
], $router->match('/contact', 'GET'));
// no prefix match, so no regex compilation necessary
$this->assertFalse($router->match('/page1', '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());
}
/**
* @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'));
}
/**
* @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 = '/(?<path>[^';
// 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'));
}
public function testMatchWithSlashBeforeOptionalPart()
{
$this->router->map('GET', '/archives/[lmin:category]?', 'Article#archives');
$expected = [
'target' => 'Article#archives',
'params' => [],
'name' => null
];
$this->assertEquals($expected, $this->router->match('/archives/', 'GET'));
$this->assertEquals($expected, $this->router->match('/archives', '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'));
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Benchmark Altorouter
*
* Usage: php ./tests/benchmark-parse-api.php <iterations>
*
* Options:
*
* <iterations>:
* The number of routes to map & match. Defaults to 1000.
*/
require __DIR__ . '/../vendor/autoload.php';
$routes = [
["POST", "/1/classes/[a:className]"],
["GET", "/1/classes/[a:className]/[i:objectId]"],
["PUT", "/1/classes/[a:className]/[i:objectId]"],
["GET", "/1/classes/[a:className]"],
["DELETE", "/1/classes/[a:className]/[i:objectId]"],
// Users
["POST", "/1/users"],
["GET", "/1/login"],
["GET", "/1/users/[i:objectId]"],
["PUT", "/1/users/[i:objectId]"],
["GET", "/1/users"],
["DELETE", "/1/users/[i:objectId]"],
["POST", "/1/requestPasswordReset"],
// Roles
["POST", "/1/roles"],
["GET", "/1/roles/[i:objectId]"],
["PUT", "/1/roles/[i:objectId]"],
["GET", "/1/roles"],
["DELETE", "/1/roles/[i:objectId]"],
// Files
["POST", "/1/files/:fileName"],
// Analytics
["POST", "/1/events/[a:eventName]"],
// Push Notifications
["POST", "/1/push"],
// Installations
["POST", "/1/installations"],
["GET", "/1/installations/[i:objectId]"],
["PUT", "/1/installations/[i:objectId]"],
["GET", "/1/installations"],
["DELETE", "/1/installations/[i:objectId]"],
// Cloud Functions
["POST", "/1/functions"],
];
$total_time = 0;
$router = new AltoRouter();
// map requests
$start = microtime(true);
foreach ($routes as $r) {
$router->map($r[0], $r[1], '');
}
$end = microtime(true);
$time = $end - $start;
$total_time += $time;
echo sprintf('Map time: %.3f ms', $time * 1000) . PHP_EOL;
// match a static route
$start = microtime(true);
$router->match('/1/login', 'GET');
$end = microtime(true);
$time = $end - $start;
$total_time += $time;
echo sprintf('Match time (existing route, no params): %.3f ms', $time * 1000) . PHP_EOL;
// match a route with 1 parameter
$start = microtime(true);
$res = $router->match('/1/classes/foo', 'GET');
$end = microtime(true);
$time = $end - $start;
$total_time += $time;
echo sprintf('Match time (existing route, 1 param): %.3f ms', $time * 1000) . PHP_EOL;
// match a route with 2 parameters
$start = microtime(true);
$res = $router->match('/1/classes/foo/500', 'GET');
$end = microtime(true);
$time = $end - $start;
$total_time += $time;
echo sprintf('Match time (existing route, 2 params): %.3f ms', $time * 1000) . PHP_EOL;
// match unexisting route
$start = microtime(true);
$router->match('/55-foo-bar', 'GET');
$end = microtime(true);
$time = $end - $start;
$total_time += $time;
echo sprintf('Match time (unexisting route): %.3f ms', $time * 1000) . PHP_EOL;
// print totals
echo sprintf('Total time: %.3f ms', $total_time * 1000) . 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;