├── src └── Routing │ ├── MatchedRoute.php │ ├── Utils.php │ ├── Request.php │ ├── Router.php │ ├── UrlGenerator.php │ └── UrlMatcher.php ├── .gitattributes ├── composer.json ├── phpunit.xml.dist ├── tests ├── RouterTest.php ├── RequestTest.php ├── UrlGenereatorTest.php └── UrlMatcherTest.php ├── .gitignore └── README.md /src/Routing/MatchedRoute.php: -------------------------------------------------------------------------------- 1 | controller = $controller; 13 | $this->parameters = $parameters; 14 | } 15 | 16 | public function getController() 17 | { 18 | return $this->controller; 19 | } 20 | 21 | public function getParameters() 22 | { 23 | return $this->parameters; 24 | } 25 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itlessons/php-routing", 3 | "type": "library", 4 | "description": "PHP Routing Library", 5 | "keywords": ["routing", "router", "URL", "URI"], 6 | "homepage": "https://github.com/itlessons/php-routing", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "itlessons", 11 | "homepage": "http://www.itlessons.info" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.3" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Routing\\": "src/Routing" 20 | } 21 | }, 22 | "version": "0.0.1", 23 | "minimum-stability": "dev" 24 | } -------------------------------------------------------------------------------- /src/Routing/Utils.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ./tests/ 23 | 24 | 25 | 26 | 27 | 28 | benchmark 29 | 30 | 31 | 32 | 33 | 34 | ./src/Routing/ 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | host); 10 | $router->add('blog', '/blog/(page:num:?)', 'app:blog:index'); 11 | 12 | $this->assertSame('/blog/1', $router->generate('blog', array('page' => 1))); 13 | $this->assertSame('/blog', $router->generate('blog')); 14 | 15 | $route = $router->match('GET', '/blog'); 16 | $this->assertSame('app:blog:index', $route != null ? $route->getController() : 'false'); 17 | } 18 | 19 | public function testCacheToFile() 20 | { 21 | $router = new \Routing\Router($this->host); 22 | 23 | $fileG = __DIR__ . '/cache/generator.cached.inc.php'; 24 | $fileM = __DIR__ . '/cache/routes.cached.inc.php'; 25 | 26 | if (is_file($fileG)) 27 | unlink($fileG); 28 | 29 | if (is_file($fileM)) 30 | unlink($fileM); 31 | 32 | $router->useCache($fileM, $fileG); 33 | $router->add('blog', '/blog/(page:num:?)', 'app:blog:index'); 34 | $router->add('user', '/id(id:num)', 'app:user:index'); 35 | 36 | $route = $router->match('GET', '/id888'); 37 | $this->assertSame('app:user:index', $route != null ? $route->getController() : 'false'); 38 | $this->assertSame('/id777', $router->generate('user', array('id' => 777))); 39 | $this->assertTrue(is_file($fileG)); 40 | $this->assertTrue(is_file($fileM)); 41 | 42 | $router = new \Routing\Router($this->host); 43 | $router->useCache($fileM, $fileG); 44 | $route = $router->match('GET', '/id888'); 45 | $this->assertSame('app:user:index', $route != null ? $route->getController() : 'false'); 46 | $this->assertSame('/id777', $router->generate('user', array('id' => 777))); 47 | $this->assertTrue(is_file($fileG)); 48 | $this->assertTrue(is_file($fileM)); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Routing/Request.php: -------------------------------------------------------------------------------- 1 | data = $data != null ? $data : $_SERVER; 13 | } 14 | 15 | public function isPost() 16 | { 17 | return $this->get('REQUEST_METHOD') == 'POST'; 18 | } 19 | 20 | public function isMethod($method) 21 | { 22 | return $this->getMethod() == strtoupper($method); 23 | } 24 | 25 | public function isHTTPS() 26 | { 27 | return $this->has('HTTPS') && $this->get('HTTPS') != 'off'; 28 | } 29 | 30 | public function getMethod() 31 | { 32 | $method = $this->get('REQUEST_METHOD'); 33 | 34 | if ($this->isPost()) { 35 | if ($this->has('X-HTTP-METHOD-OVERRIDE')) { 36 | $method = strtoupper($this->get('X-HTTP-METHOD-OVERRIDE')); 37 | } 38 | } 39 | 40 | return $method; 41 | } 42 | 43 | public function getHTTPHost() 44 | { 45 | $host = $this->isHTTPS() ? 'https://' : 'http://'; 46 | $host .= $this->getHost(); 47 | return $host; 48 | } 49 | 50 | public function getHost() 51 | { 52 | $host = $this->get('HTTP_HOST'); 53 | 54 | $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); 55 | 56 | if ($host && !preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host)) { 57 | throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host)); 58 | } 59 | 60 | return $host; 61 | } 62 | 63 | public function getPathInfo($baseUrl = null) 64 | { 65 | if (null != $this->pathInfo) 66 | return $this->pathInfo; 67 | 68 | $pathInfo = $this->get('REQUEST_URI'); 69 | 70 | if (!$pathInfo) { 71 | $pathInfo = '/'; 72 | } 73 | 74 | $schemeAndHttpHost = $this->isHTTPS() ? 'https://' : 'http://'; 75 | $schemeAndHttpHost .= $this->get('HTTP_HOST'); 76 | 77 | if (strpos($pathInfo, $schemeAndHttpHost) === 0) { 78 | $pathInfo = substr($pathInfo, strlen($schemeAndHttpHost)); 79 | } 80 | 81 | if ($pos = strpos($pathInfo, '?')) { 82 | $pathInfo = substr($pathInfo, 0, $pos); 83 | } 84 | 85 | if (null != $baseUrl) { 86 | $pathInfo = substr($pathInfo, strlen($pathInfo)); 87 | } 88 | 89 | if (!$pathInfo) { 90 | $pathInfo = '/'; 91 | } 92 | 93 | return $this->pathInfo = $pathInfo; 94 | } 95 | 96 | public function get($name) 97 | { 98 | return isset($this->data[$name]) ? $this->data[$name] : null; 99 | } 100 | 101 | public function has($name) 102 | { 103 | return $this->get($name) != null; 104 | } 105 | } -------------------------------------------------------------------------------- /tests/RequestTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\Routing\Request', $request); 10 | $this->assertEquals('/foo', $request->getPathInfo()); 11 | $this->assertEquals('GET', $request->getMethod()); 12 | $this->assertEquals('test.com', $request->getHost()); 13 | $this->assertEquals('http://test.com', $request->getHTTPHost()); 14 | $this->assertFalse($request->isHTTPS()); 15 | $this->assertFalse($request->isPost()); 16 | 17 | $request = self::create('https://test.com/foo/var/10?bar=baz', 'POST'); 18 | $this->assertEquals('/foo/var/10', $request->getPathInfo()); 19 | $this->assertTrue($request->isHTTPS()); 20 | $this->assertTrue($request->isPost()); 21 | } 22 | 23 | private static function create($uri, $method = 'GET', $server = array()) 24 | { 25 | $server = array_replace(array( 26 | 'SERVER_NAME' => 'localhost', 27 | 'SERVER_PORT' => 80, 28 | 'HTTP_HOST' => 'localhost', 29 | 'HTTP_USER_AGENT' => 'PHP-routing request', 30 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 31 | 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5', 32 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 33 | 'REMOTE_ADDR' => '127.0.0.1', 34 | 'SCRIPT_NAME' => '', 35 | 'SCRIPT_FILENAME' => '', 36 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 37 | 'REQUEST_TIME' => time(), 38 | ), $server); 39 | 40 | $server['REQUEST_URI'] = $uri; 41 | $server['PATH_INFO'] = ''; 42 | $server['REQUEST_METHOD'] = strtoupper($method); 43 | 44 | $components = parse_url($uri); 45 | if (isset($components['host'])) { 46 | $server['SERVER_NAME'] = $components['host']; 47 | $server['HTTP_HOST'] = $components['host']; 48 | } 49 | 50 | if (isset($components['scheme'])) { 51 | if ('https' === $components['scheme']) { 52 | $server['HTTPS'] = 'on'; 53 | $server['SERVER_PORT'] = 443; 54 | } else { 55 | unset($server['HTTPS']); 56 | $server['SERVER_PORT'] = 80; 57 | } 58 | } 59 | 60 | if (isset($components['port'])) { 61 | $server['SERVER_PORT'] = $components['port']; 62 | $server['HTTP_HOST'] = $server['HTTP_HOST'] . ':' . $components['port']; 63 | } 64 | 65 | if (isset($components['user'])) { 66 | $server['PHP_AUTH_USER'] = $components['user']; 67 | } 68 | 69 | if (isset($components['pass'])) { 70 | $server['PHP_AUTH_PW'] = $components['pass']; 71 | } 72 | 73 | return new \Routing\Request($server); 74 | } 75 | } -------------------------------------------------------------------------------- /tests/UrlGenereatorTest.php: -------------------------------------------------------------------------------- 1 | host); 10 | $generator->add('home', '/'); 11 | 12 | $this->assertSame('/', $generator->generate('home')); 13 | $this->assertSame($this->host . '/', $generator->generate('home', array(), true)); 14 | } 15 | 16 | /** 17 | * @expectedException InvalidArgumentException 18 | */ 19 | public function testExceptionWrongRouteName() 20 | { 21 | $generator = new \Routing\UrlGenerator($this->host); 22 | $generator->add('home', '/'); 23 | 24 | $generator->generate('wrong_name'); 25 | } 26 | 27 | /** 28 | * @expectedException InvalidArgumentException 29 | */ 30 | public function testExceptionMissingParameters() 31 | { 32 | $generator = new \Routing\UrlGenerator($this->host); 33 | $generator->add('user', '/id(:id)'); 34 | 35 | $generator->generate('user', array('page' => 1)); 36 | } 37 | 38 | public function testExtraParameters() 39 | { 40 | $generator = new \Routing\UrlGenerator($this->host); 41 | $generator->add('user', '/id(:id)'); 42 | 43 | $url = $generator->generate('user', array('id' => 777, 'page' => 1)); 44 | 45 | $this->assertSame('/id777?page=1', $url); 46 | } 47 | 48 | public function testGenerate() 49 | { 50 | $generator = new \Routing\UrlGenerator($this->host); 51 | $generator->add('user', '/id(:id).html'); 52 | $generator->add('confirm', '/confirm/(:user_id)-(:code)'); 53 | $generator->add('blog', '/blog/(:page:?)'); 54 | 55 | $this->assertSame('/id888.html', $generator->generate('user', array('id' => 888))); 56 | 57 | $this->assertSame( 58 | '/confirm/6-some-code88', $generator->generate( 59 | 'confirm', 60 | array( 61 | 'user_id' => 6, 62 | 'code' => 'some-code88' 63 | ) 64 | )); 65 | 66 | $this->assertSame('/blog/1', $generator->generate('blog', array('page' => 1))); 67 | $this->assertSame('/blog', $generator->generate('blog')); 68 | } 69 | 70 | public function testCacheToFile() 71 | { 72 | $file = __DIR__ . '/cache/generator.cached.inc.php'; 73 | $host = 'http://domain.tld'; 74 | 75 | if (is_file($file)) 76 | unlink($file); 77 | 78 | $gen = new \Routing\UrlGenerator($host); 79 | 80 | if (!$gen->loadFromFile($file)) { 81 | $gen->add('user', '/id(:id).html'); 82 | $gen->add('confirm', '/confirm/(:user_id)-(:code)'); 83 | $gen->add('blog', '/blog/(:page:?)'); 84 | $gen->dumpToFile($file); 85 | } 86 | 87 | $this->assertSame('/id888.html', $gen->generate('user', array('id' => 888))); 88 | 89 | $gen = new \Routing\UrlGenerator($host); 90 | $gen->loadFromFile($file); 91 | $this->assertSame('/id777.html', $gen->generate('user', array('id' => 777))); 92 | } 93 | } -------------------------------------------------------------------------------- /src/Routing/Router.php: -------------------------------------------------------------------------------- 1 | host = $host; 17 | } 18 | 19 | 20 | public function add($name, $pattern, $controller, $method = 'GET') 21 | { 22 | $this->routes[$name] = array( 23 | 'pattern' => $pattern, 24 | 'controller' => $controller, 25 | 'method' => $method, 26 | ); 27 | } 28 | 29 | /** 30 | * @param $method 31 | * @param $uri 32 | * @return MatchedRoute 33 | */ 34 | public function match($method, $uri) 35 | { 36 | return $this->getMatcher()->match($method, $uri); 37 | } 38 | 39 | public function generate($name, array $parameters = array(), $absolute = false) 40 | { 41 | return $this->getGenerator()->generate($name, $parameters, $absolute); 42 | } 43 | 44 | /** 45 | * @return UrlMatcher 46 | */ 47 | public function getMatcher() 48 | { 49 | if (null == $this->matcher) { 50 | $this->matcher = new UrlMatcher(); 51 | 52 | if ($this->matcherCacheFile && $this->matcher->loadFromFile($this->matcherCacheFile)) { 53 | return $this->matcher; 54 | } 55 | 56 | foreach ($this->routes as $route) { 57 | $this->matcher->register($route['method'], $route['pattern'], $route['controller']); 58 | } 59 | 60 | if ($this->matcherCacheFile) { 61 | $this->matcher->dumpToFile($this->matcherCacheFile); 62 | } 63 | 64 | } 65 | 66 | return $this->matcher; 67 | } 68 | 69 | /** 70 | * @return UrlGenerator 71 | */ 72 | public function getGenerator() 73 | { 74 | if (null == $this->generator) { 75 | $this->generator = new UrlGenerator($this->host); 76 | 77 | if ($this->generatorCacheFile && $this->generator->loadFromFile($this->generatorCacheFile)) { 78 | return $this->generator; 79 | } 80 | 81 | foreach ($this->routes as $name => $route) { 82 | $pattern = preg_replace('#\((\w+):(\w+):\?\)#', '(:$1:?)', $route['pattern']); 83 | $pattern = preg_replace('#\((\w+):(\w+)\)#', '(:$1)', $pattern); 84 | $this->generator->add($name, $pattern); 85 | } 86 | 87 | if ($this->generatorCacheFile) { 88 | $this->generator->dumpToFile($this->generatorCacheFile); 89 | } 90 | } 91 | 92 | return $this->generator; 93 | } 94 | 95 | public function useCache($matcherCacheFile, $generatorCacheFile) 96 | { 97 | $this->matcherCacheFile = $matcherCacheFile; 98 | $this->generatorCacheFile = $generatorCacheFile; 99 | } 100 | 101 | public function setHost($host) 102 | { 103 | $this->host = $host; 104 | 105 | if ($this->generator) { 106 | $this->generator->setHost($host); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | composer.phar 4 | phpunit.xml 5 | .idea 6 | tests/cache/* 7 | 8 | ################# 9 | ## Eclipse 10 | ################# 11 | 12 | *.pydevproject 13 | .project 14 | .metadata 15 | bin/ 16 | tmp/ 17 | *.tmp 18 | *.bak 19 | *.swp 20 | *~.nib 21 | local.properties 22 | .classpath 23 | .settings/ 24 | .loadpath 25 | 26 | # External tool builders 27 | .externalToolBuilders/ 28 | 29 | # Locally stored "Eclipse launch configurations" 30 | *.launch 31 | 32 | # CDT-specific 33 | .cproject 34 | 35 | # PDT-specific 36 | .buildpath 37 | 38 | 39 | ################# 40 | ## Visual Studio 41 | ################# 42 | 43 | ## Ignore Visual Studio temporary files, build results, and 44 | ## files generated by popular Visual Studio add-ons. 45 | 46 | # User-specific files 47 | *.suo 48 | *.user 49 | *.sln.docstates 50 | 51 | # Build results 52 | 53 | [Dd]ebug/ 54 | [Rr]elease/ 55 | x64/ 56 | build/ 57 | [Bb]in/ 58 | [Oo]bj/ 59 | 60 | # MSTest test Results 61 | [Tt]est[Rr]esult*/ 62 | [Bb]uild[Ll]og.* 63 | 64 | *_i.c 65 | *_p.c 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.pch 70 | *.pdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.log 86 | *.scc 87 | 88 | # Visual C++ cache files 89 | ipch/ 90 | *.aps 91 | *.ncb 92 | *.opensdf 93 | *.sdf 94 | *.cachefile 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | *.ncrunch* 116 | .*crunch*.local.xml 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.Publish.xml 136 | *.pubxml 137 | 138 | # NuGet Packages Directory 139 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 140 | #packages/ 141 | 142 | # Windows Azure Build Output 143 | csx 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.[Pp]ublish.xml 158 | *.pfx 159 | *.publishsettings 160 | 161 | # RIA/Silverlight projects 162 | Generated_Code/ 163 | 164 | # Backup & report files from converting an old project file to a newer 165 | # Visual Studio version. Backup files are not needed, because we have git ;-) 166 | _UpgradeReport_Files/ 167 | Backup*/ 168 | UpgradeLog*.XML 169 | UpgradeLog*.htm 170 | 171 | # SQL Server files 172 | App_Data/*.mdf 173 | App_Data/*.ldf 174 | 175 | ############# 176 | ## Windows detritus 177 | ############# 178 | 179 | # Windows image file caches 180 | Thumbs.db 181 | ehthumbs.db 182 | 183 | # Folder config file 184 | Desktop.ini 185 | 186 | # Recycle Bin used on file shares 187 | $RECYCLE.BIN/ 188 | 189 | # Mac crap 190 | .DS_Store 191 | 192 | 193 | ############# 194 | ## Python 195 | ############# 196 | 197 | *.py[co] 198 | 199 | # Packages 200 | *.egg 201 | *.egg-info 202 | dist/ 203 | build/ 204 | eggs/ 205 | parts/ 206 | var/ 207 | sdist/ 208 | develop-eggs/ 209 | .installed.cfg 210 | 211 | # Installer logs 212 | pip-log.txt 213 | 214 | # Unit test / coverage reports 215 | .coverage 216 | .tox 217 | 218 | #Translations 219 | *.mo 220 | 221 | #Mr Developer 222 | .mr.developer.cfg 223 | cases.txt 224 | 225 | -------------------------------------------------------------------------------- /src/Routing/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | host = $host; 15 | } 16 | 17 | public function add($name, $pattern) 18 | { 19 | $this->map[$name] = $pattern; 20 | } 21 | 22 | public function has($name) 23 | { 24 | return isset($this->map[$name]); 25 | } 26 | 27 | /** 28 | * Generate url by route. 29 | * 30 | * @param string $name 31 | * @param array $parameters 32 | * @param boolean $absolute 33 | * @throws \InvalidArgumentException 34 | * @return string 35 | */ 36 | public function generate($name, array $parameters = array(), $absolute = false) 37 | { 38 | if (!$this->has($name)) { 39 | throw new \InvalidArgumentException(sprintf('Rule for identifier "%s" not found', $name)); 40 | } 41 | 42 | $this->compilePattern($name); 43 | 44 | if (($diff = array_diff_key($this->mapData[$name], $parameters))) { 45 | throw new \InvalidArgumentException(sprintf( 46 | 'The "%s" route has some missing parameters ("%s").', 47 | $name, 48 | implode('", "', array_keys($diff)) 49 | )); 50 | } 51 | 52 | $pattern = $this->map[$name]; 53 | $rParameters = array(); 54 | $extra = array(); 55 | 56 | foreach ($parameters as $k => $v) { 57 | if (isset($this->mapData[$name][$k])) { 58 | $rName = '(:' . $k . ')'; 59 | $rParameters[$rName] = $v; 60 | } elseif (isset($this->mapOptionalData[$name][$k])) { 61 | $rName = '(:' . $k . ':?)'; 62 | $rParameters[$rName] = $v; 63 | } else { 64 | $extra[$k] = $v; 65 | } 66 | } 67 | 68 | $url = strtr($pattern, $rParameters); 69 | 70 | // clean blank optional placeholder 71 | if (false !== strpos($url, '/(:')) { 72 | $url = preg_replace('#/\(:(\w+):\?\)$#', '', $url); 73 | } 74 | 75 | if (count($extra)) { 76 | $url .= '?' . http_build_query($extra); 77 | } 78 | 79 | if ($absolute) { 80 | $url = $this->host . $url; 81 | } 82 | 83 | return $url; 84 | } 85 | 86 | private function compilePattern($name) 87 | { 88 | if (isset($this->mapData[$name])) { 89 | return; 90 | } 91 | 92 | $pattern = $this->map[$name]; 93 | $matches = array(); 94 | 95 | $this->mapData[$name] = array(); 96 | $this->mapOptionalData[$name] = array(); 97 | 98 | if (preg_match_all('#\(:(\w+)\)#', $pattern, $matches)) { 99 | $this->mapData[$name] = array_flip($matches[1]); 100 | } elseif (preg_match_all('#/\(:(\w+):\?\)$#', $pattern, $matches)) { 101 | $this->mapOptionalData[$name] = array_flip($matches[1]); 102 | } 103 | } 104 | 105 | public function dumpToFile($file) 106 | { 107 | //preCompile 108 | foreach ($this->map as $name => $v) { 109 | $this->compilePattern($name); 110 | } 111 | 112 | $code = 'map, true) . ',' 114 | . var_export($this->mapData, true) . ',' 115 | . var_export($this->mapOptionalData, true) 116 | . ');'; 117 | 118 | Utils::writeFile($file, $code); 119 | } 120 | 121 | /** 122 | * @param $file 123 | * @return bool 124 | */ 125 | public function loadFromFile($file) 126 | { 127 | if (is_file($file)) { 128 | list($this->map, $this->mapData, $this->mapOptionalData) = require $file; 129 | return true; 130 | } 131 | 132 | return false; 133 | } 134 | 135 | public function setHost($host) 136 | { 137 | $this->host = $host; 138 | } 139 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP Routing Library 2 | =================== 3 | 4 | Routing associates a request with the code that will convert it to a response. 5 | 6 | The example below demonstrates how you can set up a fully working routing 7 | system: 8 | 9 | use Routing\Router; 10 | 11 | $host = 'http://domain.tld'; 12 | 13 | $router = new Router($host); 14 | 15 | $router->add('home', '/', 'controller:action'); 16 | $router->add('hello', '/hello', 'static:welcome', 'GET'); 17 | $router->add('profile', '/user(id:num)', 'profile:index', 'GET|POST'); 18 | 19 | $route = $router->match('GET', '/user777'); 20 | // $route->getController() => 'static:welcome' 21 | // $route->getParameters() => [id:777] 22 | 23 | $url = $router->generate('profile', array('id' => 777)); 24 | // $url => /user777 25 | 26 | $url = $router->generate('profile', array('id' => 777), true); 27 | // $url => http://domain.tld/user777 28 | 29 | 30 | URL Matching Only 31 | ----------------- 32 | 33 | You can use url matcher standalone: 34 | 35 | use Routing\UrlMatcher; 36 | 37 | $matcher = new UrlMatcher(); 38 | $matcher->register('GET', '/', 'controller:action'); 39 | $matcher->register('GET', '/hello', 'static:welcome'); 40 | $matcher->register('GET|POST', '/user(id:num)', 'profile:index'); 41 | 42 | $route = $router->match('GET', '/hello'); 43 | 44 | 45 | URL Generating Only 46 | ------------------- 47 | 48 | You can use url generator standalone: 49 | 50 | use Routing\UrlGenerator; 51 | 52 | $generator = new UrlGenerator('http://domain.tld'); 53 | $generator->add('home', '/'); 54 | $generator->add('hello', '/hello'); 55 | $generator->add('profile', '/user(:id)'); 56 | 57 | $url = $generator->generate('profile', array('id' => 888), true); 58 | 59 | 60 | Optional Last Placeholder 61 | ------------------------- 62 | 63 | You can specify optional placeholder in the end of pattern: 64 | 65 | use Routing\Router; 66 | 67 | $host = 'http://domain.tld'; 68 | 69 | $router = new Router($host); 70 | $router->add('blog', '/blog/(page:num:?)', 'controller:action'); 71 | 72 | // match 73 | $route = $router->match('GET', '/blog'); 74 | $route = $router->match('GET', '/blog/1'); 75 | 76 | // generate 77 | $router->generate('blog'); => /blog 78 | $router->generate('blog', array('page' => 1)); => /blog/1 79 | 80 | 81 | Similar Routes 82 | -------------- 83 | 84 | You can use redirect on similar route (e.g /blog -> /blog/ if /blog/ exists): 85 | 86 | use Routing\Router; 87 | 88 | $host = 'http://domain.tld'; 89 | 90 | $router = new Router($host); 91 | $router->add('home', '/', 'controller:action'); 92 | $router->add('hello', '/hello', 'static:welcome'); 93 | $router->add('profile', '/blog/', 'profile:index'); 94 | 95 | $route = $router->match('GET', '/hello/'); 96 | if($router->getMatcher()->isNeedRedirect()){ 97 | // need redirect to /hello 98 | redirect($router->getMatcher()->getRedirectUrl(), 302); 99 | } 100 | 101 | $route = $router->match('GET', '/blog'); 102 | if($router->getMatcher()->isNeedRedirect()){ 103 | // need redirect to /blog/ 104 | redirect($router->getMatcher()->getRedirectUrl(), 302); 105 | } 106 | 107 | Cache Compiled Data 108 | ------------------- 109 | 110 | You can cache compiled rules to files for performance: 111 | 112 | use Routing\Router; 113 | use Routing\Request; 114 | 115 | $request = new Request(); 116 | 117 | $router = new Router($request->getHTTPHost()); 118 | $router->useCache(__DIR__.'/matcher.cache.php', __DIR__.'/generator.cache.php'); 119 | $router->add('home', '/', 'controller:action'); 120 | // ... 121 | $route = $router->match($request->getMethod(), $request->getPathInfo()); 122 | 123 | 124 | Request Class Helper 125 | -------------------- 126 | 127 | You can use simple request class to find pathInfo: 128 | 129 | use Routing\Router; 130 | use Routing\Request; 131 | 132 | $request = new Request(); 133 | 134 | $router = new Router($request->getHTTPHost()); 135 | 136 | $router->add('home', '/', 'controller:action'); 137 | $router->add('hello', '/hello', 'static:welcome', 'GET'); 138 | $router->add('profile', '/user(id:num)', 'profile:index', 'GET|POST'); 139 | 140 | $route = $router->match($request->getMethod(), $request->getPathInfo()); 141 | 142 | 143 | Resources 144 | --------- 145 | 146 | You can run the unit tests with the following command: 147 | 148 | $ cd path/to/php-routing/ 149 | $ composer.phar install 150 | $ phpunit 151 | 152 | Links 153 | ----- 154 | * [Система роутинга на сайте с помощью PHP](http://www.itlessons.info/php/routing-library/) 155 | * [Base example source](http://demos.itlessons.info/res/024-php-routing.zip) 156 | -------------------------------------------------------------------------------- /src/Routing/UrlMatcher.php: -------------------------------------------------------------------------------- 1 | array(), 17 | 'POST' => array(), 18 | 'PUT' => array(), 19 | 'DELETE' => array(), 20 | 'PATCH' => array(), 21 | 'HEAD' => array(), 22 | ); 23 | 24 | private $patterns = array( 25 | 'num' => '[0-9]+', 26 | 'str' => '[a-zA-Z\.\-_%]+', 27 | 'any' => '[a-zA-Z0-9\.\-_%]+', 28 | ); 29 | 30 | private $redirectUrl; 31 | 32 | public function addPattern($name, $pattern) 33 | { 34 | $this->patterns[$name] = $pattern; 35 | } 36 | 37 | public function register($method, $route, $controller) 38 | { 39 | $methods = strtoupper($method); 40 | 41 | if (false !== strpos($methods, '|')) { 42 | $methods = explode('|', $methods); 43 | } 44 | 45 | if ($methods == '*') { 46 | $methods = $this->methods; 47 | } 48 | 49 | $methods = (array)$methods; 50 | 51 | $converted = $this->convertRoute($route); 52 | 53 | foreach ($methods as $m) { 54 | $this->routes[$m][$converted] = $controller; 55 | } 56 | } 57 | 58 | private function convertRoute($route) 59 | { 60 | if (false === strpos($route, '(')) { 61 | return $route; 62 | } 63 | 64 | if (preg_match('#^/\((\w+):(\w+):\?\)$#', $route)) { 65 | throw new \InvalidArgumentException(sprintf('Prefix required when use optional placeholder in route "%s"', $route)); 66 | } 67 | 68 | $parse = preg_replace_callback('#/\((\w+):(\w+):\?\)$#', array($this, 'replaceOptionalRoute'), $route); 69 | 70 | return preg_replace_callback('#\((\w+):(\w+)\)#', array($this, 'replaceRoute'), $parse); 71 | } 72 | 73 | private function replaceOptionalRoute($match) 74 | { 75 | $name = $match[1]; 76 | $pattern = $match[2]; 77 | 78 | return '(?:/(?<' . $name . '>' . strtr($pattern, $this->patterns) . '))?'; 79 | } 80 | 81 | private function replaceRoute($match) 82 | { 83 | $name = $match[1]; 84 | $pattern = $match[2]; 85 | 86 | return '(?<' . $name . '>' . strtr($pattern, $this->patterns) . ')'; 87 | } 88 | 89 | /** 90 | * @param $method 91 | * @param $uri 92 | * @return MatchedRoute 93 | */ 94 | public function match($method, $uri) 95 | { 96 | $this->redirectUrl = null; 97 | $method = strtoupper($method); 98 | $route = $this->doMatch($method, $uri); 99 | 100 | if ($route != null) 101 | return $route; 102 | 103 | if ($method == 'GET') 104 | $this->tryFindUrlToRedirect($uri); 105 | } 106 | 107 | /** 108 | * Try find similar url, e.g. 109 | * /blog/ -> /blog if /blog exists 110 | * /blog -> /blog/ if /blog/ exists 111 | * @param $uri 112 | */ 113 | private function tryFindUrlToRedirect($uri) 114 | { 115 | $tmpUri = $uri . '/'; 116 | 117 | if (substr($uri, -1) === '/') 118 | $tmpUri = rtrim($uri, '/'); 119 | 120 | $route = $this->doMatch('GET', $tmpUri); 121 | if ($route) 122 | $this->redirectUrl = $tmpUri; 123 | } 124 | 125 | public function isNeedRedirect() 126 | { 127 | return $this->redirectUrl != null; 128 | } 129 | 130 | public function getRedirectUrl() 131 | { 132 | return $this->redirectUrl; 133 | } 134 | 135 | private function routes($method) 136 | { 137 | return isset($this->routes[$method]) ? $this->routes[$method] : array(); 138 | } 139 | 140 | private function doMatch($method, $uri) 141 | { 142 | $routes = $this->routes($method); 143 | 144 | if (array_key_exists($uri, $routes)) { 145 | return new MatchedRoute($routes[$uri]); 146 | } 147 | 148 | foreach ($routes as $route => $controller) { 149 | if (false !== strpos($route, '(')) { 150 | $pattern = '#^' . $route . '$#s'; 151 | 152 | if (preg_match($pattern, $uri, $parameters)) { 153 | return new MatchedRoute($controller, $this->processParameters($parameters)); 154 | } 155 | } 156 | } 157 | } 158 | 159 | private function processParameters($parameters) 160 | { 161 | foreach ($parameters as $k => $v) { 162 | if (is_int($k)) { 163 | unset($parameters[$k]); 164 | } 165 | } 166 | 167 | return $parameters; 168 | } 169 | 170 | public function dumpToFile($file) 171 | { 172 | $code = 'routes, true) . ';'; 173 | Utils::writeFile($file, $code); 174 | } 175 | 176 | /** 177 | * @param $file 178 | * @return bool 179 | */ 180 | public function loadFromFile($file) 181 | { 182 | if (is_file($file)) { 183 | $this->routes = require $file; 184 | return true; 185 | } 186 | 187 | return false; 188 | } 189 | } -------------------------------------------------------------------------------- /tests/UrlMatcherTest.php: -------------------------------------------------------------------------------- 1 | register('GET', '/auth', 'app:auth:index'); 9 | $matcher->register('GET', '/', 'app:home:index'); 10 | 11 | $route = $matcher->match('GET', '/'); 12 | 13 | $this->assertInstanceOf('\\Routing\\MatchedRoute', $route); 14 | $this->assertSame('app:home:index', $route->getController()); 15 | } 16 | 17 | public function testMatchRequestMethods() 18 | { 19 | $matcher = new \Routing\UrlMatcher(); 20 | $matcher->register('GET|POST', '/auth', 'app:auth:index'); 21 | $matcher->register('GET', '/', 'app:home:index'); 22 | 23 | $route = $matcher->match('POST', '/auth'); 24 | $this->assertSame('app:auth:index', $route->getController()); 25 | 26 | $route = $matcher->match('GET', '/auth'); 27 | $this->assertSame('app:auth:index', $route->getController()); 28 | 29 | $route = $matcher->match('PUT', '/auth'); 30 | $this->assertNull($route); 31 | } 32 | 33 | public function testMatchPatterns() 34 | { 35 | $matcher = new \Routing\UrlMatcher(); 36 | $matcher->register('GET', '/id(id:num)', 'app:user:index'); 37 | $matcher->register('GET', '/search/(query:str)', 'app:search:index'); 38 | $matcher->register('GET', '/tag/(tag:any)', 'app:tag:index'); 39 | $matcher->register('GET', '/', 'app:home:index'); 40 | $matcher->register('GET', '/blog/', 'app:blog:index'); 41 | $matcher->register('GET', '/some/(page:num:?)', 'app:some:index'); 42 | 43 | $route = $matcher->match('GET', '/id777'); 44 | $this->assertSame('app:user:index', $route->getController()); 45 | 46 | $route = $matcher->match('GET', '/id-777'); 47 | $this->assertNull($route); 48 | 49 | $route = $matcher->match('GET', '/id72d'); 50 | $this->assertNull($route); 51 | 52 | $route = $matcher->match('GET', '/search/12'); 53 | $this->assertNull($route); 54 | 55 | $route = $matcher->match('GET', '/search/someword'); 56 | $this->assertSame('app:search:index', $route->getController()); 57 | 58 | $route = $matcher->match('GET', '/search/some-word'); 59 | $this->assertSame('app:search:index', $route->getController()); 60 | 61 | $route = $matcher->match('GET', '/search/someword89'); 62 | $this->assertNull($route); 63 | 64 | $route = $matcher->match('GET', '/tag/so-mew_ord90'); 65 | $this->assertSame('app:tag:index', $route->getController()); 66 | 67 | $route = $matcher->match('GET', '/tag/so-mew_ord90/'); 68 | $this->assertNull($route); 69 | $this->assertTrue($matcher->isNeedRedirect()); 70 | $this->assertSame('/tag/so-mew_ord90', $matcher->getRedirectUrl()); 71 | 72 | $route = $matcher->match('GET', '/id777/'); 73 | $this->assertNull($route); 74 | $this->assertSame('/id777', $matcher->getRedirectUrl()); 75 | 76 | $route = $matcher->match('GET', '/blog'); 77 | $this->assertNull($route); 78 | $this->assertSame('/blog/', $matcher->getRedirectUrl()); 79 | 80 | $route = $matcher->match('GET', '/blog/'); 81 | $this->assertSame('app:blog:index', $route->getController()); 82 | 83 | $route = $matcher->match('GET', '/some/1'); 84 | $this->assertSame('app:some:index', $route != null ? $route->getController() : 'false'); 85 | 86 | $route = $matcher->match('GET', '/some'); 87 | $this->assertSame('app:some:index', $route != null ? $route->getController() : 'false'); 88 | 89 | $route = $matcher->match('GET', '/some/'); 90 | $this->assertNull($route); 91 | $this->assertSame('/some', $matcher->getRedirectUrl()); 92 | } 93 | 94 | /** 95 | * @expectedException InvalidArgumentException 96 | */ 97 | public function testExceptionOptionalPlaceholder() 98 | { 99 | $matcher = new \Routing\UrlMatcher(); 100 | $matcher->register('GET', '/(id:num:?)', 'app:some:index'); 101 | } 102 | 103 | public function testCacheToFile() 104 | { 105 | $file = __DIR__ . '/cache/routes.cached.inc.php'; 106 | 107 | if (is_file($file)) 108 | unlink($file); 109 | 110 | $matcher = new \Routing\UrlMatcher(); 111 | 112 | if (!$matcher->loadFromFile($file)) { 113 | $matcher->register('GET', '/id(id:num)', 'app:user:index'); 114 | $matcher->register('GET', '/search/(query:str)', 'app:search:index'); 115 | $matcher->register('GET', '/tag/(tag:any)', 'app:tag:index'); 116 | $matcher->register('GET', '/', 'app:home:index'); 117 | $matcher->register('GET', '/blog/', 'app:blog:index'); 118 | $matcher->register('GET', '/some/(page:num:?)', 'app:some:index'); 119 | $matcher->dumpToFile($file); 120 | } 121 | 122 | $route = $matcher->match('GET', '/search/some_str'); 123 | 124 | $this->assertTrue(is_file($file)); 125 | $this->assertSame('app:search:index', $route != null ? $route->getController() : 'false'); 126 | 127 | $matcher = new \Routing\UrlMatcher(); 128 | $matcher->loadFromFile($file); 129 | $this->assertSame('app:search:index', $route != null ? $route->getController() : 'false'); 130 | } 131 | } --------------------------------------------------------------------------------