├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .php_cs.dist ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── demo-multilang ├── .htaccess └── index.php ├── demo ├── .htaccess └── index.php ├── phpunit.xml.dist ├── src └── Bramus │ └── Router │ └── Router.php └── tests ├── RouterTest.php └── bootstrap.php /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: Tests on PHP ${{ matrix.php }} ${{ matrix.dependencies }} 10 | runs-on: ubuntu-20.04 11 | container: 12 | image: shivammathur/node:2004 13 | strategy: 14 | matrix: 15 | php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4'] 16 | dependencies: ['', '--prefer-lowest --prefer-stable'] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | tools: composer:2.2 24 | - name: Install dependencies 25 | run: composer update --no-interaction --prefer-dist ${{ matrix.dependencies }} 26 | # - name: Configure PHPUnit problem matchers 27 | # run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 28 | - name: Run tests 29 | run: ./vendor/bin/phpunit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /composer.lock 3 | /vendor/ 4 | tests-report/ 5 | /.idea/ 6 | .php_cs.cache -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | exclude('node_modules') 23 | ->exclude('vendor') 24 | ->in(__DIR__); 25 | 26 | return PhpCsFixer\Config::create() 27 | ->setRules([ 28 | '@PSR2' => true, 29 | 'array_syntax' => [ 'syntax' => 'long' ], 30 | 'binary_operator_spaces' => [ 'align_equals' => false, 'align_double_arrow' => false ], 31 | 'cast_spaces' => true, 32 | 'combine_consecutive_unsets' => true, 33 | 'concat_space' => [ 'spacing' => 'one' ], 34 | 'linebreak_after_opening_tag' => true, 35 | 'no_blank_lines_after_class_opening' => true, 36 | 'no_blank_lines_after_phpdoc' => true, 37 | 'no_extra_consecutive_blank_lines' => true, 38 | 'no_trailing_comma_in_singleline_array' => true, 39 | 'no_whitespace_in_blank_line' => true, 40 | 'no_spaces_around_offset' => true, 41 | 'no_unused_imports' => true, 42 | 'no_useless_else' => true, 43 | 'no_useless_return' => true, 44 | 'no_whitespace_before_comma_in_array' => true, 45 | 'normalize_index_brace' => true, 46 | 'phpdoc_indent' => true, 47 | 'phpdoc_to_comment' => true, 48 | 'phpdoc_trim' => true, 49 | 'single_quote' => true, 50 | 'ternary_to_null_coalescing' => true, 51 | 'trailing_comma_in_multiline_array' => true, 52 | 'trim_array_spaces' => true, 53 | 'method_argument_space' => ['ensure_fully_multiline' => false], 54 | 'no_break_comment' => false, 55 | 'blank_line_before_statement' => true, 56 | ]) 57 | ->setFinder($finder); 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `bramus/router` 2 | 3 | ## 1.next – ????.??.?? 4 | 5 | ## 1.6.1 – 2021.11.19 6 | 7 | - Fixed: Fix `trigger404()` to work without custom 404 handler _([#169](https://github.com/bramus/router/pull/169))_ _(@mjoris)_ 8 | 9 | ## 1.6 – 2021.07.23 10 | 11 | - Added: Ability to set multiple 404s, depending on the route prefix _(@uvulpos)_ 12 | 13 | ## 1.5 – 2020.10.26 14 | 15 | - Fixed: Correctly invoke static/non-static class methods _(@bramus)_ 16 | - Fixed: Fix PHP 5.3 support _(@cikal)_ 17 | - Fixed: Fix arguments in demo _(@khromov)_ 18 | - Fixed: Fix #72 _(@acicali)_ 19 | - Added: PHP 7.4 support _(@ShaneMcC)_ 20 | - Added: Ability to externally trigger a 404 _(@PlanetTheCloud)_ 21 | 22 | ## 1.4.2 – 2019.02.27 23 | 24 | - Fixed: Play nice with emoji in base paths ([ref](https://github.com/bramus/router/commit/8692190532db269882f83d27cea95d4f22a50da2#commitcomment-32492636), [ref](https://github.com/bramus/router/commit/492444d84fde7e54551ff0bf8ca79ff9292094da#commitcomment-32496820)) _(@bramus)_ 25 | - Added: Extra Tests _(@bramus)_ 26 | 27 | ## 1.4.1 – 2019.02.26 28 | 29 | - Fixed: Fix bug where Cyrillic charges and Emojis in placeholder were urlencoded (see [#80](https://github.com/bramus/router/issues/80#issuecomment-467154490)) _(@bramus)_ 30 | - Fixed: Make `bramus/router` play nice with situations where the entry script and entry URLs are not coupled (see [#82](https://github.com/bramus/router/issues/82#issuecomment-466956078)) _(@bramus)_ 31 | - Changed: Changed visibility of `getBasePath` and `getCurrentUri` to being `public` _(@bramus)_ 32 | 33 | ## 1.4 – 2019.02.18 34 | 35 | - Added: Support for Cyrillic chars and Emoji in placeholder values and placeholder names (see [#80](https://github.com/bramus/router/issues/80)) _(@bramus)_ 36 | - Added: `composer test` shorthand _(@bramus)_ 37 | - Added: Changelog _(@bramus)_ 38 | - Changed: Documentation Improvements _(@bramus)_ 39 | 40 | ## 1.3.1 – 2017.12.22 41 | 42 | - Added: Extra Tests _(@bramus)_ 43 | - Changed: Documentation Improvements _(@artyuum)_ 44 | 45 | ## 1.3 – 2017.12.21 46 | 47 | - Added: Support `Class@method` callbacks in `set404()` _(@bramus)_ 48 | - Changed: Refactored callback invocation _(@bramus)_ 49 | - Changed: Documentation Improvements _(@artyuum)_ 50 | 51 | ## 1.2.1 – 2017.10.06 52 | 53 | - Changed: Documentation Improvements _(@bramus)_ 54 | 55 | ## 1.2 – 2017.10.06 56 | 57 | - Added: Support route matching using _“placeholders”_ (e.g. curly braces) _(@ovflowd)_ 58 | - Added: Default Namespace Capability using `setNamespace()`, for use with `Class@Method` calls _(@ovflowd)_ 59 | - Added: Extra Tests _(@bramus)_ 60 | - Bugfix: Make sure callable are actually callable _(@ovflowd)_ 61 | - Demo: Added a multilang demo _(@bramus)_ 62 | - Changed: Documentation Improvements _(@lai0n)_ 63 | 64 | ## 1.1 – 2016.05.26 65 | 66 | - Added: Return `true` if a route was handled, `false` otherwise _(@tleb)_ 67 | - Added: `getBasePath()` _(@ovflowd)_ 68 | - Added: Support `Class@Method` calls _(@ovflowd)_ 69 | - Changed: Tweak a few method signaturs so that they're protected _(@tleb)_ 70 | - Changed: Documentation Improvements _(@tleb)_ 71 | 72 | ## 1.0 – 2015.02.04 73 | 74 | - First 1.x release 75 | 76 | ## _(Unversioned Releases)_ – 2013.04.08 - 2015.02.04 77 | 78 | - Initial release with suppport for: 79 | - Static and Dynamic Route Handling 80 | - Shorthands: `get()`, `post()`, `put()`, `delete()`, and `options()` 81 | - Before Route Middlewares / Before Route Middlewares: `before()` 82 | - After Router Middlewares / Run Callback 83 | 84 | - Added: Optional Route Patterns 85 | - Added: Subrouting (mount callables onto a subroute/prefix) 86 | - Added: `patch()` shorthand 87 | - Added: Support for `X-HTTP-Method-Override` header 88 | - Bugfix: Use the HTTP version as found in `['SERVER_PROTOCOL']` 89 | - Bugfix: Nested Subpatterns / Multiple Matching _(@jbleuzen)_ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Bram(us) Van Damme - http://www.bram.us/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bramus/router 2 | 3 | [![Build Status](https://github.com/bramus/router/workflows/CI/badge.svg)](https://github.com/bramus/router/actions) [![Source](http://img.shields.io/badge/source-bramus/router-blue.svg?style=flat-square)](https://github.com/bramus/router) [![Version](https://img.shields.io/packagist/v/bramus/router.svg?style=flat-square)](https://packagist.org/packages/bramus/router) [![Downloads](https://img.shields.io/packagist/dt/bramus/router.svg?style=flat-square)](https://packagist.org/packages/bramus/router/stats) [![License](https://img.shields.io/packagist/l/bramus/router.svg?style=flat-square)](https://github.com/bramus/router/blob/master/LICENSE) 4 | 5 | A lightweight and simple object oriented PHP Router. 6 | Built by Bram(us) Van Damme _([https://www.bram.us](https://www.bram.us))_ and [Contributors](https://github.com/bramus/router/graphs/contributors) 7 | 8 | 9 | ## Features 10 | 11 | - Supports `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH` and `HEAD` request methods 12 | - [Routing shorthands such as `get()`, `post()`, `put()`, …](#routing-shorthands) 13 | - [Static Route Patterns](#route-patterns) 14 | - Dynamic Route Patterns: [Dynamic PCRE-based Route Patterns](#dynamic-pcre-based-route-patterns) or [Dynamic Placeholder-based Route Patterns](#dynamic-placeholder-based-route-patterns) 15 | - [Optional Route Subpatterns](#optional-route-subpatterns) 16 | - [Supports `X-HTTP-Method-Override` header](#overriding-the-request-method) 17 | - [Subrouting / Mounting Routes](#subrouting--mounting-routes) 18 | - [Allowance of `Class@Method` calls](#classmethod-calls) 19 | - [Custom 404 handling](#custom-404) 20 | - [Before Route Middlewares](#before-route-middlewares) 21 | - [Before Router Middlewares / Before App Middlewares](#before-router-middlewares) 22 | - [After Router Middleware / After App Middleware (Finish Callback)](#after-router-middleware--run-callback) 23 | - [Works fine in subfolders](#subfolder-support) 24 | 25 | 26 | 27 | ## Prerequisites/Requirements 28 | 29 | - PHP 5.3 or greater 30 | - [URL Rewriting](https://gist.github.com/bramus/5332525) 31 | 32 | 33 | 34 | ## Installation 35 | 36 | Installation is possible using Composer 37 | 38 | ``` 39 | composer require bramus/router ~1.6 40 | ``` 41 | 42 | 43 | 44 | ## Demo 45 | 46 | A demo is included in the `demo` subfolder. Serve it using your favorite web server, or using PHP 5.4+'s built-in server by executing `php -S localhost:8080` on the shell. A `.htaccess` for use with Apache is included. 47 | 48 | Additionally a demo of a mutilingual router is also included. This can be found in the `demo-multilang` subfolder and can be ran in the same manner as the normal demo. 49 | 50 | ## Usage 51 | 52 | Create an instance of `\Bramus\Router\Router`, define some routes onto it, and run it. 53 | 54 | ```php 55 | // Require composer autoloader 56 | require __DIR__ . '/vendor/autoload.php'; 57 | 58 | // Create Router instance 59 | $router = new \Bramus\Router\Router(); 60 | 61 | // Define routes 62 | // ... 63 | 64 | // Run it! 65 | $router->run(); 66 | ``` 67 | 68 | 69 | ### Routing 70 | 71 | Hook __routes__ (a combination of one or more HTTP methods and a pattern) using `$router->match(method(s), pattern, function)`: 72 | 73 | ```php 74 | $router->match('GET|POST', 'pattern', function() { … }); 75 | ``` 76 | 77 | `bramus/router` supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` _(see [note](#a-note-on-making-head-requests))_, and `OPTIONS` HTTP request methods. Pass in a single request method, or multiple request methods separated by `|`. 78 | 79 | When a route matches against the current URL (e.g. `$_SERVER['REQUEST_URI']`), the attached __route handling function__ will be executed. The route handling function must be a [callable](http://php.net/manual/en/language.types.callable.php). Only the first route matched will be handled. When no matching route is found, a 404 handler will be executed. 80 | 81 | ### Routing Shorthands 82 | 83 | Shorthands for single request methods are provided: 84 | 85 | ```php 86 | $router->get('pattern', function() { /* ... */ }); 87 | $router->post('pattern', function() { /* ... */ }); 88 | $router->put('pattern', function() { /* ... */ }); 89 | $router->delete('pattern', function() { /* ... */ }); 90 | $router->options('pattern', function() { /* ... */ }); 91 | $router->patch('pattern', function() { /* ... */ }); 92 | ``` 93 | 94 | You can use this shorthand for a route that can be accessed using any method: 95 | 96 | ```php 97 | $router->all('pattern', function() { … }); 98 | ``` 99 | 100 | Note: Routes must be hooked before `$router->run();` is being called. 101 | 102 | Note: There is no shorthand for `match()` as `bramus/router` will internally re-route such requrests to their equivalent `GET` request, in order to comply with RFC2616 _(see [note](#a-note-on-making-head-requests))_. 103 | 104 | ### Route Patterns 105 | 106 | Route Patterns can be static or dynamic: 107 | 108 | - __Static Route Patterns__ contain no dynamic parts and must match exactly against the `path` part of the current URL. 109 | - __Dynamic Route Patterns__ contain dynamic parts that can vary per request. The varying parts are named __subpatterns__ and are defined using either Perl-compatible regular expressions (PCRE) or by using __placeholders__ 110 | 111 | #### Static Route Patterns 112 | 113 | A static route pattern is a regular string representing a URI. It will be compared directly against the `path` part of the current URL. 114 | 115 | Examples: 116 | 117 | - `/about` 118 | - `/contact` 119 | 120 | Usage Examples: 121 | 122 | ```php 123 | // This route handling function will only be executed when visiting http(s)://www.example.org/about 124 | $router->get('/about', function() { 125 | echo 'About Page Contents'; 126 | }); 127 | ``` 128 | 129 | #### Dynamic PCRE-based Route Patterns 130 | 131 | This type of Route Patterns contain dynamic parts which can vary per request. The varying parts are named __subpatterns__ and are defined using regular expressions. 132 | 133 | Examples: 134 | 135 | - `/movies/(\d+)` 136 | - `/profile/(\w+)` 137 | 138 | Commonly used PCRE-based subpatterns within Dynamic Route Patterns are: 139 | 140 | - `\d+` = One or more digits (0-9) 141 | - `\w+` = One or more word characters (a-z 0-9 _) 142 | - `[a-z0-9_-]+` = One or more word characters (a-z 0-9 _) and the dash (-) 143 | - `.*` = Any character (including `/`), zero or more 144 | - `[^/]+` = Any character but `/`, one or more 145 | 146 | Note: The [PHP PCRE Cheat Sheet](https://courses.cs.washington.edu/courses/cse154/15sp/cheat-sheets/php-regex-cheat-sheet.pdf) might come in handy. 147 | 148 | The __subpatterns__ defined in Dynamic PCRE-based Route Patterns are converted to parameters which are passed into the route handling function. Prerequisite is that these subpatterns need to be defined as __parenthesized subpatterns__, which means that they should be wrapped between parens: 149 | 150 | ```php 151 | // Bad 152 | $router->get('/hello/\w+', function($name) { 153 | echo 'Hello ' . htmlentities($name); 154 | }); 155 | 156 | // Good 157 | $router->get('/hello/(\w+)', function($name) { 158 | echo 'Hello ' . htmlentities($name); 159 | }); 160 | ``` 161 | 162 | Note: The leading `/` at the very beginning of a route pattern is not mandatory, but is recommended. 163 | 164 | When multiple subpatterns are defined, the resulting __route handling parameters__ are passed into the route handling function in the order they are defined in: 165 | 166 | ```php 167 | $router->get('/movies/(\d+)/photos/(\d+)', function($movieId, $photoId) { 168 | echo 'Movie #' . $movieId . ', photo #' . $photoId; 169 | }); 170 | ``` 171 | 172 | #### Dynamic Placeholder-based Route Patterns 173 | 174 | This type of Route Patterns are the same as __Dynamic PCRE-based Route Patterns__, but with one difference: they don't use regexes to do the pattern matching but they use the more easy __placeholders__ instead. Placeholders are strings surrounded by curly braces, e.g. `{name}`. You don't need to add parens around placeholders. 175 | 176 | Examples: 177 | 178 | - `/movies/{id}` 179 | - `/profile/{username}` 180 | 181 | Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (`.*`). 182 | 183 | ```php 184 | $router->get('/movies/{movieId}/photos/{photoId}', function($movieId, $photoId) { 185 | echo 'Movie #' . $movieId . ', photo #' . $photoId; 186 | }); 187 | ``` 188 | 189 | Note: the name of the placeholder does not need to match with the name of the parameter that is passed into the route handling function: 190 | 191 | ```php 192 | $router->get('/movies/{foo}/photos/{bar}', function($movieId, $photoId) { 193 | echo 'Movie #' . $movieId . ', photo #' . $photoId; 194 | }); 195 | ``` 196 | 197 | 198 | ### Optional Route Subpatterns 199 | 200 | Route subpatterns can be made optional by making the subpatterns optional by adding a `?` after them. Think of blog URLs in the form of `/blog(/year)(/month)(/day)(/slug)`: 201 | 202 | ```php 203 | $router->get( 204 | '/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?', 205 | function($year = null, $month = null, $day = null, $slug = null) { 206 | if (!$year) { echo 'Blog overview'; return; } 207 | if (!$month) { echo 'Blog year overview'; return; } 208 | if (!$day) { echo 'Blog month overview'; return; } 209 | if (!$slug) { echo 'Blog day overview'; return; } 210 | echo 'Blogpost ' . htmlentities($slug) . ' detail'; 211 | } 212 | ); 213 | ``` 214 | 215 | The code snippet above responds to the URLs `/blog`, `/blog/year`, `/blog/year/month`, `/blog/year/month/day`, and `/blog/year/month/day/slug`. 216 | 217 | Note: With optional parameters it is important that the leading `/` of the subpatterns is put inside the subpattern itself. Don't forget to set default values for the optional parameters. 218 | 219 | The code snipped above unfortunately also responds to URLs like `/blog/foo` and states that the overview needs to be shown - which is incorrect. Optional subpatterns can be made successive by extending the parenthesized subpatterns so that they contain the other optional subpatterns: The pattern should resemble `/blog(/year(/month(/day(/slug))))` instead of the previous `/blog(/year)(/month)(/day)(/slug)`: 220 | 221 | ```php 222 | $router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) { 223 | // ... 224 | }); 225 | ``` 226 | 227 | Note: It is highly recommended to __always__ define successive optional parameters. 228 | 229 | To make things complete use [quantifiers](http://www.php.net/manual/en/regexp.reference.repetition.php) to require the correct amount of numbers in the URL: 230 | 231 | ```php 232 | $router->get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) { 233 | // ... 234 | }); 235 | ``` 236 | 237 | 238 | ### Subrouting / Mounting Routes 239 | 240 | Use `$router->mount($baseroute, $fn)` to mount a collection of routes onto a subroute pattern. The subroute pattern is prefixed onto all following routes defined in the scope. e.g. Mounting a callback `$fn` onto `/movies` will prefix `/movies` onto all following routes. 241 | 242 | ```php 243 | $router->mount('/movies', function() use ($router) { 244 | 245 | // will result in '/movies/' 246 | $router->get('/', function() { 247 | echo 'movies overview'; 248 | }); 249 | 250 | // will result in '/movies/id' 251 | $router->get('/(\d+)', function($id) { 252 | echo 'movie id ' . htmlentities($id); 253 | }); 254 | 255 | }); 256 | ``` 257 | 258 | Nesting of subroutes is possible, just define a second `$router->mount()` in the callable that's already contained within a preceding `$router->mount()`. 259 | 260 | 261 | ### `Class@Method` calls 262 | 263 | We can route to the class action like so: 264 | 265 | ```php 266 | $router->get('/(\d+)', '\App\Controllers\User@showProfile'); 267 | ``` 268 | 269 | When a request matches the specified route URI, the `showProfile` method on the `User` class will be executed. The defined route parameters will be passed to the class method. 270 | 271 | The method can be static (recommended) or non-static (not-recommended). In case of a non-static method, a new instance of the class will be created. 272 | 273 | If most/all of your handling classes are in one and the same namespace, you can set the default namespace to use on your router instance via `setNamespace()` 274 | 275 | ```php 276 | $router->setNamespace('\App\Controllers'); 277 | $router->get('/users/(\d+)', 'User@showProfile'); 278 | $router->get('/cars/(\d+)', 'Car@showProfile'); 279 | ``` 280 | 281 | ### Custom 404 282 | 283 | The default 404 handler sets a 404 status code and exits. You can override this default 404 handler by using `$router->set404(callable);` 284 | 285 | ```php 286 | $router->set404(function() { 287 | header('HTTP/1.1 404 Not Found'); 288 | // ... do something special here 289 | }); 290 | ``` 291 | 292 | You can also define multiple custom routes e.x. you want to define an `/api` route, you can print a custom 404 page: 293 | 294 | ```php 295 | $router->set404('/api(/.*)?', function() { 296 | header('HTTP/1.1 404 Not Found'); 297 | header('Content-Type: application/json'); 298 | 299 | $jsonArray = array(); 300 | $jsonArray['status'] = "404"; 301 | $jsonArray['status_text'] = "route not defined"; 302 | 303 | echo json_encode($jsonArray); 304 | }); 305 | ``` 306 | 307 | Also supported are `Class@Method` callables: 308 | 309 | ```php 310 | $router->set404('\App\Controllers\Error@notFound'); 311 | ``` 312 | 313 | The 404 handler will be executed when no route pattern was matched to the current URL. 314 | 315 | 💡 You can also manually trigger the 404 handler by calling `$router->trigger404()` 316 | 317 | ```php 318 | $router->get('/([a-z0-9-]+)', function($id) use ($router) { 319 | if (!Posts::exists($id)) { 320 | $router->trigger404(); 321 | return; 322 | } 323 | 324 | // … 325 | }); 326 | ``` 327 | 328 | 329 | ### Before Route Middlewares 330 | 331 | `bramus/router` supports __Before Route Middlewares__, which are executed before the route handling is processed. 332 | 333 | Like route handling functions, you hook a handling function to a combination of one or more HTTP request methods and a specific route pattern. 334 | 335 | ```php 336 | $router->before('GET|POST', '/admin/.*', function() { 337 | if (!isset($_SESSION['user'])) { 338 | header('location: /auth/login'); 339 | exit(); 340 | } 341 | }); 342 | ``` 343 | 344 | Unlike route handling functions, more than one before route middleware is executed when more than one route match is found. 345 | 346 | 347 | ### Before Router Middlewares 348 | 349 | Before route middlewares are route specific. Using a general route pattern (viz. _all URLs_), they can become __Before Router Middlewares__ _(in other projects sometimes referred to as before app middlewares)_ which are always executed, no matter what the requested URL is. 350 | 351 | ```php 352 | $router->before('GET', '/.*', function() { 353 | // ... this will always be executed 354 | }); 355 | ``` 356 | 357 | 358 | ### After Router Middleware / Run Callback 359 | 360 | Run one (1) middleware function, name the __After Router Middleware__ _(in other projects sometimes referred to as after app middlewares)_ after the routing was processed. Just pass it along the `$router->run()` function. The run callback is route independent. 361 | 362 | ```php 363 | $router->run(function() { … }); 364 | ``` 365 | 366 | Note: If the route handling function has `exit()`ed the run callback won't be run. 367 | 368 | 369 | ### Overriding the request method 370 | 371 | Use `X-HTTP-Method-Override` to override the HTTP Request Method. Only works when the original Request Method is `POST`. Allowed values for `X-HTTP-Method-Override` are `PUT`, `DELETE`, or `PATCH`. 372 | 373 | 374 | ### Subfolder support 375 | 376 | Out-of-the box `bramus/router` will run in any (sub)folder you place it into … no adjustments to your code are needed. You can freely move your _entry script_ `index.php` around, and the router will automatically adapt itself to work relatively from the current folder's path by mounting all routes onto that __basePath__. 377 | 378 | Say you have a server hosting the domain `www.example.org` using `public_html/` as its document root, with this little _entry script_ `index.php`: 379 | 380 | ```php 381 | $router->get('/', function() { echo 'Index'; }); 382 | $router->get('/hello', function() { echo 'Hello!'; }); 383 | ``` 384 | 385 | - If your were to place this file _(along with its accompanying `.htaccess` file or the like)_ at the document root level (e.g. `public_html/index.php`), `bramus/router` will mount all routes onto the domain root (e.g. `/`) and thus respond to `https://www.example.org/` and `https://www.example.org/hello`. 386 | 387 | - If you were to move this file _(along with its accompanying `.htaccess` file or the like)_ into a subfolder (e.g. `public_html/demo/index.php`), `bramus/router` will mount all routes onto the current path (e.g. `/demo`) and thus repsond to `https://www.example.org/demo` and `https://www.example.org/demo/hello`. There's **no** need for `$router->mount(…)` in this case. 388 | 389 | #### Disabling subfolder support 390 | 391 | In case you **don't** want `bramus/router` to automatically adapt itself to the folder its being placed in, it's possible to manually override the _basePath_ by calling `setBasePath()`. This is necessary in the _(uncommon)_ situation where your _entry script_ and your _entry URLs_ are not tightly coupled _(e.g. when the entry script is placed into a subfolder that does not need be part of the URLs it responds to)_. 392 | 393 | ```php 394 | // Override auto base path detection 395 | $router->setBasePath('/'); 396 | 397 | $router->get('/', function() { echo 'Index'; }); 398 | $router->get('/hello', function() { echo 'Hello!'; }); 399 | 400 | $router->run(); 401 | ``` 402 | 403 | If you were to place this file into a subfolder (e.g. `public_html/some/sub/folder/index.php`), it will still mount the routes onto the domain root (e.g. `/`) and thus respond to `https://www.example.org/` and `https://www.example.org/hello` _(given that your `.htaccess` file – placed at the document root level – rewrites requests to it)_ 404 | 405 | ## Integration with other libraries 406 | 407 | Integrate other libraries with `bramus/router` by making good use of the `use` keyword to pass dependencies into the handling functions. 408 | 409 | ```php 410 | $tpl = new \Acme\Template\Template(); 411 | 412 | $router->get('/', function() use ($tpl) { 413 | $tpl->load('home.tpl'); 414 | $tpl->setdata(array( 415 | 'name' => 'Bramus!' 416 | )); 417 | }); 418 | 419 | $router->run(function() use ($tpl) { 420 | $tpl->display(); 421 | }); 422 | ``` 423 | 424 | Given this structure it is still possible to manipulate the output from within the After Router Middleware 425 | 426 | 427 | ## A note on working with PUT 428 | 429 | There's no such thing as `$_PUT` in PHP. One must fake it: 430 | 431 | ```php 432 | $router->put('/movies/(\d+)', function($id) { 433 | 434 | // Fake $_PUT 435 | $_PUT = array(); 436 | parse_str(file_get_contents('php://input'), $_PUT); 437 | 438 | // ... 439 | 440 | }); 441 | ``` 442 | 443 | 444 | ## A note on making HEAD requests 445 | 446 | When making `HEAD` requests all output will be buffered to prevent any content trickling into the response body, as defined in [RFC2616 (Hypertext Transfer Protocol -- HTTP/1.1)](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4): 447 | 448 | > The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request. This method can be used for obtaining metainformation about the entity implied by the request without transferring the entity-body itself. This method is often used for testing hypertext links for validity, accessibility, and recent modification. 449 | 450 | To achieve this, `bramus/router` but will internally re-route `HEAD` requests to their equivalent `GET` request and automatically suppress all output. 451 | 452 | 453 | ## Unit Testing & Code Coverage 454 | 455 | `bramus/router` ships with unit tests using [PHPUnit](https://github.com/sebastianbergmann/phpunit/). 456 | 457 | - If PHPUnit is installed globally run `phpunit` to run the tests. 458 | 459 | - If PHPUnit is not installed globally, install it locally throuh composer by running `composer install --dev`. Run the tests themselves by calling `vendor/bin/phpunit`. 460 | 461 | The included `composer.json` will also install `php-code-coverage` which allows one to generate a __Code Coverage Report__. Run `phpunit --coverage-html ./tests-report` (XDebug required), a report will be placed into the `tests-report` subfolder. 462 | 463 | 464 | ## Acknowledgements 465 | 466 | `bramus/router` is inspired upon [Klein](https://github.com/chriso/klein.php), [Ham](https://github.com/radiosilence/Ham), and [JREAM/route](https://bitbucket.org/JREAM/route) . Whilst Klein provides lots of features it is not object oriented. Whilst Ham is Object Oriented, it's bad at _separation of concerns_ as it also provides templating within the routing class. Whilst JREAM/route is a good starting point it is limited in what it does (only GET routes for example). 467 | 468 | 469 | 470 | ## License 471 | 472 | `bramus/router` is released under the MIT public license. See the enclosed `LICENSE` for details. 473 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bramus/router", 3 | "description": "A lightweight and simple object oriented PHP Router", 4 | "keywords": ["router", "routing"], 5 | "homepage": "https://github.com/bramus/router", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Bram(us) Van Damme", 11 | "email": "bramus@bram.us", 12 | "homepage": "http://www.bram.us" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.3.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~4.8", 20 | "phpunit/php-code-coverage": "~2.0", 21 | "friendsofphp/php-cs-fixer": "~2.14" 22 | }, 23 | "autoload": { 24 | "psr-0": {"Bramus": "src/"} 25 | }, 26 | "scripts": { 27 | "test": "./vendor/bin/phpunit --colors=always", 28 | "lint": "php-cs-fixer fix --diff --dry-run", 29 | "fix": "php-cs-fixer fix" 30 | } 31 | } -------------------------------------------------------------------------------- /demo-multilang/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule . index.php [L] -------------------------------------------------------------------------------- /demo-multilang/index.php: -------------------------------------------------------------------------------- 1 | allowedLanguages = $allowedLanguages; 40 | $this->defaultLanguage = (in_array($defaultLanguage, $allowedLanguages) ? $defaultLanguage : $allowedLanguages[0]); 41 | 42 | // Visiting the root? Redirect to the default language index 43 | $this->match('GET|POST|PUT|DELETE|HEAD', '/', function () { 44 | header('location: /' . $this->defaultLanguage); 45 | exit(); 46 | }); 47 | 48 | // Create a before handler to make sure the language checks out when visiting anything but the root. 49 | // If the language doesn't check out, redirect to the default language index 50 | $this->before('GET|POST|PUT|DELETE|HEAD', '/([a-z0-9_-]+)(/.*)?', function ($language, $slug = null) { 51 | 52 | // The given language does not appear in the array of allowed languages 53 | if (!in_array($language, $this->allowedLanguages)) { 54 | header('location: /' . $this->defaultLanguage); 55 | exit(); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | // Create a Router 62 | $router = new MultilangRouter( 63 | array('en','nl','fr'), //= allowed languages 64 | 'nl' // = default language 65 | ); 66 | 67 | $router->get('/([a-z0-9_-]+)', function ($language) { 68 | exit('This is the ' . htmlentities($language) . ' index'); 69 | }); 70 | 71 | $router->get('/([a-z0-9_-]+)/([a-z0-9_-]+)', function ($language, $slug) { 72 | exit('This is the ' . htmlentities($language) . ' version of ' . htmlentities($slug)); 73 | }); 74 | 75 | $router->get('/([a-z0-9_-]+)/(.*)', function ($language, $slug) { 76 | exit('This is the ' . htmlentities($language) . ' version of ' . htmlentities($slug) . ' (multiple segments allowed)'); 77 | }); 78 | 79 | // Thunderbirds are go! 80 | $router->run(); 81 | 82 | // EOF 83 | -------------------------------------------------------------------------------- /demo/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule . index.php [L] -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | set404(function () { 18 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 19 | echo '404, route not found!'; 20 | }); 21 | 22 | // custom 404 23 | $router->set404('/test(/.*)?', function () { 24 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 25 | echo '

404, route not found!

'; 26 | }); 27 | 28 | $router->set404('/api(/.*)?', function() { 29 | header('HTTP/1.1 404 Not Found'); 30 | header('Content-Type: application/json'); 31 | 32 | $jsonArray = array(); 33 | $jsonArray['status'] = "404"; 34 | $jsonArray['status_text'] = "route not defined"; 35 | 36 | echo json_encode($jsonArray); 37 | }); 38 | 39 | // Before Router Middleware 40 | $router->before('GET', '/.*', function () { 41 | header('X-Powered-By: bramus/router'); 42 | }); 43 | 44 | // Static route: / (homepage) 45 | $router->get('/', function () { 46 | echo '

bramus/router

47 |

Try these routes:

48 |

57 |

58 |

Custom error routes

59 | 64 | '; 65 | }); 66 | 67 | // Static route: /hello 68 | $router->get('/hello', function () { 69 | echo '

bramus/router

Visit /hello/name to get your Hello World mojo on!

'; 70 | }); 71 | 72 | // Dynamic route: /hello/name 73 | $router->get('/hello/(\w+)', function ($name) { 74 | echo 'Hello ' . htmlentities($name); 75 | }); 76 | 77 | // Dynamic route: /ohai/name/in/parts 78 | $router->get('/ohai/(.*)', function ($url) { 79 | echo 'Ohai ' . htmlentities($url); 80 | }); 81 | 82 | // Dynamic route with (successive) optional subpatterns: /blog(/year(/month(/day(/slug)))) 83 | $router->get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function ($year = null, $month = null, $day = null, $slug = null) { 84 | if (!$year) { 85 | echo 'Blog overview'; 86 | 87 | return; 88 | } 89 | if (!$month) { 90 | echo 'Blog year overview (' . $year . ')'; 91 | 92 | return; 93 | } 94 | if (!$day) { 95 | echo 'Blog month overview (' . $year . '-' . $month . ')'; 96 | 97 | return; 98 | } 99 | if (!$slug) { 100 | echo 'Blog day overview (' . $year . '-' . $month . '-' . $day . ')'; 101 | 102 | return; 103 | } 104 | echo 'Blogpost ' . htmlentities($slug) . ' detail (' . $year . '-' . $month . '-' . $day . ')'; 105 | }); 106 | 107 | // Subrouting 108 | $router->mount('/movies', function () use ($router) { 109 | 110 | // will result in '/movies' 111 | $router->get('/', function () { 112 | echo 'movies overview'; 113 | }); 114 | 115 | // will result in '/movies' 116 | $router->post('/', function () { 117 | echo 'add movie'; 118 | }); 119 | 120 | // will result in '/movies/id' 121 | $router->get('/(\d+)', function ($id) { 122 | echo 'movie id ' . htmlentities($id); 123 | }); 124 | 125 | // will result in '/movies/id' 126 | $router->put('/(\d+)', function ($id) { 127 | echo 'Update movie id ' . htmlentities($id); 128 | }); 129 | }); 130 | 131 | // Thunderbirds are go! 132 | $router->run(); 133 | 134 | // EOF 135 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | src/ 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Bramus/Router/Router.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright Copyright (c), 2013 Bram(us) Van Damme 6 | * @license MIT public license 7 | */ 8 | namespace Bramus\Router; 9 | 10 | /** 11 | * Class Router. 12 | */ 13 | class Router 14 | { 15 | /** 16 | * @var array The route patterns and their handling functions 17 | */ 18 | private $afterRoutes = array(); 19 | 20 | /** 21 | * @var array The before middleware route patterns and their handling functions 22 | */ 23 | private $beforeRoutes = array(); 24 | 25 | /** 26 | * @var array [object|callable] The function to be executed when no route has been matched 27 | */ 28 | protected $notFoundCallback = []; 29 | 30 | /** 31 | * @var string Current base route, used for (sub)route mounting 32 | */ 33 | private $baseRoute = ''; 34 | 35 | /** 36 | * @var string The Request Method that needs to be handled 37 | */ 38 | private $requestedMethod = ''; 39 | 40 | /** 41 | * @var string The Server Base Path for Router Execution 42 | */ 43 | private $serverBasePath; 44 | 45 | /** 46 | * @var string Default Controllers Namespace 47 | */ 48 | private $namespace = ''; 49 | 50 | /** 51 | * Store a before middleware route and a handling function to be executed when accessed using one of the specified methods. 52 | * 53 | * @param string $methods Allowed methods, | delimited 54 | * @param string $pattern A route pattern such as /about/system 55 | * @param object|callable $fn The handling function to be executed 56 | */ 57 | public function before($methods, $pattern, $fn) 58 | { 59 | $pattern = $this->baseRoute . '/' . trim($pattern, '/'); 60 | $pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern; 61 | 62 | if ($methods === '*') { 63 | $methods = 'GET|POST|PUT|DELETE|OPTIONS|PATCH|HEAD'; 64 | } 65 | 66 | foreach (explode('|', $methods) as $method) { 67 | $this->beforeRoutes[$method][] = array( 68 | 'pattern' => $pattern, 69 | 'fn' => $fn, 70 | ); 71 | } 72 | } 73 | 74 | /** 75 | * Store a route and a handling function to be executed when accessed using one of the specified methods. 76 | * 77 | * @param string $methods Allowed methods, | delimited 78 | * @param string $pattern A route pattern such as /about/system 79 | * @param object|callable $fn The handling function to be executed 80 | */ 81 | public function match($methods, $pattern, $fn) 82 | { 83 | $pattern = $this->baseRoute . '/' . trim($pattern, '/'); 84 | $pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern; 85 | 86 | foreach (explode('|', $methods) as $method) { 87 | $this->afterRoutes[$method][] = array( 88 | 'pattern' => $pattern, 89 | 'fn' => $fn, 90 | ); 91 | } 92 | } 93 | 94 | /** 95 | * Shorthand for a route accessed using any method. 96 | * 97 | * @param string $pattern A route pattern such as /about/system 98 | * @param object|callable $fn The handling function to be executed 99 | */ 100 | public function all($pattern, $fn) 101 | { 102 | $this->match('GET|POST|PUT|DELETE|OPTIONS|PATCH|HEAD', $pattern, $fn); 103 | } 104 | 105 | /** 106 | * Shorthand for a route accessed using GET. 107 | * 108 | * @param string $pattern A route pattern such as /about/system 109 | * @param object|callable $fn The handling function to be executed 110 | */ 111 | public function get($pattern, $fn) 112 | { 113 | $this->match('GET', $pattern, $fn); 114 | } 115 | 116 | /** 117 | * Shorthand for a route accessed using POST. 118 | * 119 | * @param string $pattern A route pattern such as /about/system 120 | * @param object|callable $fn The handling function to be executed 121 | */ 122 | public function post($pattern, $fn) 123 | { 124 | $this->match('POST', $pattern, $fn); 125 | } 126 | 127 | /** 128 | * Shorthand for a route accessed using PATCH. 129 | * 130 | * @param string $pattern A route pattern such as /about/system 131 | * @param object|callable $fn The handling function to be executed 132 | */ 133 | public function patch($pattern, $fn) 134 | { 135 | $this->match('PATCH', $pattern, $fn); 136 | } 137 | 138 | /** 139 | * Shorthand for a route accessed using DELETE. 140 | * 141 | * @param string $pattern A route pattern such as /about/system 142 | * @param object|callable $fn The handling function to be executed 143 | */ 144 | public function delete($pattern, $fn) 145 | { 146 | $this->match('DELETE', $pattern, $fn); 147 | } 148 | 149 | /** 150 | * Shorthand for a route accessed using PUT. 151 | * 152 | * @param string $pattern A route pattern such as /about/system 153 | * @param object|callable $fn The handling function to be executed 154 | */ 155 | public function put($pattern, $fn) 156 | { 157 | $this->match('PUT', $pattern, $fn); 158 | } 159 | 160 | /** 161 | * Shorthand for a route accessed using OPTIONS. 162 | * 163 | * @param string $pattern A route pattern such as /about/system 164 | * @param object|callable $fn The handling function to be executed 165 | */ 166 | public function options($pattern, $fn) 167 | { 168 | $this->match('OPTIONS', $pattern, $fn); 169 | } 170 | 171 | /** 172 | * Mounts a collection of callbacks onto a base route. 173 | * 174 | * @param string $baseRoute The route sub pattern to mount the callbacks on 175 | * @param callable $fn The callback method 176 | */ 177 | public function mount($baseRoute, $fn) 178 | { 179 | // Track current base route 180 | $curBaseRoute = $this->baseRoute; 181 | 182 | // Build new base route string 183 | $this->baseRoute .= $baseRoute; 184 | 185 | // Call the callable 186 | call_user_func($fn); 187 | 188 | // Restore original base route 189 | $this->baseRoute = $curBaseRoute; 190 | } 191 | 192 | /** 193 | * Get all request headers. 194 | * 195 | * @return array The request headers 196 | */ 197 | public function getRequestHeaders() 198 | { 199 | $headers = array(); 200 | 201 | // If getallheaders() is available, use that 202 | if (function_exists('getallheaders')) { 203 | $headers = getallheaders(); 204 | 205 | // getallheaders() can return false if something went wrong 206 | if ($headers !== false) { 207 | return $headers; 208 | } 209 | } 210 | 211 | // Method getallheaders() not available or went wrong: manually extract 'm 212 | foreach ($_SERVER as $name => $value) { 213 | if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) { 214 | $headers[str_replace(array(' ', 'Http'), array('-', 'HTTP'), ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; 215 | } 216 | } 217 | 218 | return $headers; 219 | } 220 | 221 | /** 222 | * Get the request method used, taking overrides into account. 223 | * 224 | * @return string The Request method to handle 225 | */ 226 | public function getRequestMethod() 227 | { 228 | // Take the method as found in $_SERVER 229 | $method = $_SERVER['REQUEST_METHOD']; 230 | 231 | // If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification 232 | // @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 233 | if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { 234 | ob_start(); 235 | $method = 'GET'; 236 | } 237 | 238 | // If it's a POST request, check for a method override header 239 | elseif ($_SERVER['REQUEST_METHOD'] == 'POST') { 240 | $headers = $this->getRequestHeaders(); 241 | if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) { 242 | $method = $headers['X-HTTP-Method-Override']; 243 | } 244 | } 245 | 246 | return $method; 247 | } 248 | 249 | /** 250 | * Set a Default Lookup Namespace for Callable methods. 251 | * 252 | * @param string $namespace A given namespace 253 | */ 254 | public function setNamespace($namespace) 255 | { 256 | if (is_string($namespace)) { 257 | $this->namespace = $namespace; 258 | } 259 | } 260 | 261 | /** 262 | * Get the given Namespace before. 263 | * 264 | * @return string The given Namespace if exists 265 | */ 266 | public function getNamespace() 267 | { 268 | return $this->namespace; 269 | } 270 | 271 | /** 272 | * Execute the router: Loop all defined before middleware's and routes, and execute the handling function if a match was found. 273 | * 274 | * @param object|callable $callback Function to be executed after a matching route was handled (= after router middleware) 275 | * 276 | * @return bool 277 | */ 278 | public function run($callback = null) 279 | { 280 | // Define which method we need to handle 281 | $this->requestedMethod = $this->getRequestMethod(); 282 | 283 | // Handle all before middlewares 284 | if (isset($this->beforeRoutes[$this->requestedMethod])) { 285 | $this->handle($this->beforeRoutes[$this->requestedMethod]); 286 | } 287 | 288 | // Handle all routes 289 | $numHandled = 0; 290 | if (isset($this->afterRoutes[$this->requestedMethod])) { 291 | $numHandled = $this->handle($this->afterRoutes[$this->requestedMethod], true); 292 | } 293 | 294 | // If no route was handled, trigger the 404 (if any) 295 | if ($numHandled === 0) { 296 | if (isset($this->afterRoutes[$this->requestedMethod])) { 297 | $this->trigger404($this->afterRoutes[$this->requestedMethod]); 298 | } else { 299 | $this->trigger404(); 300 | } 301 | } // If a route was handled, perform the finish callback (if any) 302 | elseif ($callback && is_callable($callback)) { 303 | $callback(); 304 | } 305 | 306 | // If it originally was a HEAD request, clean up after ourselves by emptying the output buffer 307 | if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { 308 | ob_end_clean(); 309 | } 310 | 311 | // Return true if a route was handled, false otherwise 312 | return $numHandled !== 0; 313 | } 314 | 315 | /** 316 | * Set the 404 handling function. 317 | * 318 | * @param object|callable|string $match_fn The function to be executed 319 | * @param object|callable $fn The function to be executed 320 | */ 321 | public function set404($match_fn, $fn = null) 322 | { 323 | if (!is_null($fn)) { 324 | $this->notFoundCallback[$match_fn] = $fn; 325 | } else { 326 | $this->notFoundCallback['/'] = $match_fn; 327 | } 328 | } 329 | 330 | /** 331 | * Triggers 404 response 332 | * 333 | * @param string $pattern A route pattern such as /about/system 334 | */ 335 | public function trigger404($match = null){ 336 | 337 | // Counter to keep track of the number of routes we've handled 338 | $numHandled = 0; 339 | 340 | // handle 404 pattern 341 | if (count($this->notFoundCallback) > 0) 342 | { 343 | // loop fallback-routes 344 | foreach ($this->notFoundCallback as $route_pattern => $route_callable) { 345 | 346 | // matches result 347 | $matches = []; 348 | 349 | // check if there is a match and get matches as $matches (pointer) 350 | $is_match = $this->patternMatches($route_pattern, $this->getCurrentUri(), $matches, PREG_OFFSET_CAPTURE); 351 | 352 | // is fallback route match? 353 | if ($is_match) { 354 | 355 | // Rework matches to only contain the matches, not the orig string 356 | $matches = array_slice($matches, 1); 357 | 358 | // Extract the matched URL parameters (and only the parameters) 359 | $params = array_map(function ($match, $index) use ($matches) { 360 | 361 | // We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE) 362 | if (isset($matches[$index + 1]) && isset($matches[$index + 1][0]) && is_array($matches[$index + 1][0])) { 363 | if ($matches[$index + 1][0][1] > -1) { 364 | return trim(substr($match[0][0], 0, $matches[$index + 1][0][1] - $match[0][1]), '/'); 365 | } 366 | } // We have no following parameters: return the whole lot 367 | 368 | return isset($match[0][0]) && $match[0][1] != -1 ? trim($match[0][0], '/') : null; 369 | }, $matches, array_keys($matches)); 370 | 371 | $this->invoke($route_callable); 372 | 373 | ++$numHandled; 374 | } 375 | } 376 | } 377 | if (($numHandled == 0) && (isset($this->notFoundCallback['/']))) { 378 | $this->invoke($this->notFoundCallback['/']); 379 | } elseif ($numHandled == 0) { 380 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 381 | } 382 | } 383 | 384 | /** 385 | * Replace all curly braces matches {} into word patterns (like Laravel) 386 | * Checks if there is a routing match 387 | * 388 | * @param $pattern 389 | * @param $uri 390 | * @param $matches 391 | * @param $flags 392 | * 393 | * @return bool -> is match yes/no 394 | */ 395 | private function patternMatches($pattern, $uri, &$matches, $flags) 396 | { 397 | // Replace all curly braces matches {} into word patterns (like Laravel) 398 | $pattern = preg_replace('/\/{(.*?)}/', '/(.*?)', $pattern); 399 | 400 | // we may have a match! 401 | return boolval(preg_match_all('#^' . $pattern . '$#', $uri, $matches, PREG_OFFSET_CAPTURE)); 402 | } 403 | 404 | /** 405 | * Handle a a set of routes: if a match is found, execute the relating handling function. 406 | * 407 | * @param array $routes Collection of route patterns and their handling functions 408 | * @param bool $quitAfterRun Does the handle function need to quit after one route was matched? 409 | * 410 | * @return int The number of routes handled 411 | */ 412 | private function handle($routes, $quitAfterRun = false) 413 | { 414 | // Counter to keep track of the number of routes we've handled 415 | $numHandled = 0; 416 | 417 | // The current page URL 418 | $uri = $this->getCurrentUri(); 419 | 420 | // Loop all routes 421 | foreach ($routes as $route) { 422 | 423 | // get routing matches 424 | $is_match = $this->patternMatches($route['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE); 425 | 426 | // is there a valid match? 427 | if ($is_match) { 428 | 429 | // Rework matches to only contain the matches, not the orig string 430 | $matches = array_slice($matches, 1); 431 | 432 | // Extract the matched URL parameters (and only the parameters) 433 | $params = array_map(function ($match, $index) use ($matches) { 434 | 435 | // We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE) 436 | if (isset($matches[$index + 1]) && isset($matches[$index + 1][0]) && is_array($matches[$index + 1][0])) { 437 | if ($matches[$index + 1][0][1] > -1) { 438 | return trim(substr($match[0][0], 0, $matches[$index + 1][0][1] - $match[0][1]), '/'); 439 | } 440 | } // We have no following parameters: return the whole lot 441 | 442 | return isset($match[0][0]) && $match[0][1] != -1 ? trim($match[0][0], '/') : null; 443 | }, $matches, array_keys($matches)); 444 | 445 | // Call the handling function with the URL parameters if the desired input is callable 446 | $this->invoke($route['fn'], $params); 447 | 448 | ++$numHandled; 449 | 450 | // If we need to quit, then quit 451 | if ($quitAfterRun) { 452 | break; 453 | } 454 | } 455 | } 456 | 457 | // Return the number of routes handled 458 | return $numHandled; 459 | } 460 | 461 | private function invoke($fn, $params = array()) 462 | { 463 | if (is_callable($fn)) { 464 | call_user_func_array($fn, $params); 465 | } 466 | 467 | // If not, check the existence of special parameters 468 | elseif (stripos($fn, '@') !== false) { 469 | // Explode segments of given route 470 | list($controller, $method) = explode('@', $fn); 471 | 472 | // Adjust controller class if namespace has been set 473 | if ($this->getNamespace() !== '') { 474 | $controller = $this->getNamespace() . '\\' . $controller; 475 | } 476 | 477 | try { 478 | $reflectedMethod = new \ReflectionMethod($controller, $method); 479 | // Make sure it's callable 480 | if ($reflectedMethod->isPublic() && (!$reflectedMethod->isAbstract())) { 481 | if ($reflectedMethod->isStatic()) { 482 | forward_static_call_array(array($controller, $method), $params); 483 | } else { 484 | // Make sure we have an instance, because a non-static method must not be called statically 485 | if (\is_string($controller)) { 486 | $controller = new $controller(); 487 | } 488 | call_user_func_array(array($controller, $method), $params); 489 | } 490 | } 491 | } catch (\ReflectionException $reflectionException) { 492 | // The controller class is not available or the class does not have the method $method 493 | } 494 | } 495 | } 496 | 497 | /** 498 | * Define the current relative URI. 499 | * 500 | * @return string 501 | */ 502 | public function getCurrentUri() 503 | { 504 | // Get the current Request URI and remove rewrite base path from it (= allows one to run the router in a sub folder) 505 | $uri = substr(rawurldecode($_SERVER['REQUEST_URI']), strlen($this->getBasePath())); 506 | 507 | // Don't take query params into account on the URL 508 | if (strstr($uri, '?')) { 509 | $uri = substr($uri, 0, strpos($uri, '?')); 510 | } 511 | 512 | // Remove trailing slash + enforce a slash at the start 513 | return '/' . trim($uri, '/'); 514 | } 515 | 516 | /** 517 | * Return server base Path, and define it if isn't defined. 518 | * 519 | * @return string 520 | */ 521 | public function getBasePath() 522 | { 523 | // Check if server base path is defined, if not define it. 524 | if ($this->serverBasePath === null) { 525 | $this->serverBasePath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/'; 526 | } 527 | 528 | return $this->serverBasePath; 529 | } 530 | 531 | /** 532 | * Explicilty sets the server base path. To be used when your entry script path differs from your entry URLs. 533 | * @see https://github.com/bramus/router/issues/82#issuecomment-466956078 534 | * 535 | * @param string 536 | */ 537 | public function setBasePath($serverBasePath) 538 | { 539 | $this->serverBasePath = $serverBasePath; 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\Bramus\Router\Router', new \Bramus\Router\Router()); 35 | } 36 | 37 | public function testUri() 38 | { 39 | // Create Router 40 | $router = new \Bramus\Router\Router(); 41 | $router->match('GET', '/about', function () { 42 | echo 'about'; 43 | }); 44 | 45 | // Fake some data 46 | $_SERVER['SCRIPT_NAME'] = '/sub/folder/index.php'; 47 | $_SERVER['REQUEST_URI'] = '/sub/folder/about/whatever'; 48 | 49 | $method = new ReflectionMethod( 50 | '\Bramus\Router\Router', 51 | 'getCurrentUri' 52 | ); 53 | 54 | $method->setAccessible(true); 55 | 56 | $this->assertEquals( 57 | '/about/whatever', 58 | $method->invoke(new \Bramus\Router\Router()) 59 | ); 60 | } 61 | 62 | public function testBasePathOverride() 63 | { 64 | // Create Router 65 | $router = new \Bramus\Router\Router(); 66 | $router->match('GET', '/about', function () { 67 | echo 'about'; 68 | }); 69 | 70 | // Fake some data 71 | $_SERVER['SCRIPT_NAME'] = '/public/index.php'; 72 | $_SERVER['REQUEST_URI'] = '/about'; 73 | 74 | $router->setBasePath('/'); 75 | 76 | $this->assertEquals( 77 | '/', 78 | $router->getBasePath() 79 | ); 80 | 81 | // Test the /about route 82 | ob_start(); 83 | $_SERVER['REQUEST_URI'] = '/about'; 84 | $router->run(); 85 | $this->assertEquals('about', ob_get_contents()); 86 | 87 | // Cleanup 88 | ob_end_clean(); 89 | } 90 | 91 | public function testBasePathThatContainsEmoji() 92 | { 93 | // Create Router 94 | $router = new \Bramus\Router\Router(); 95 | $router->match('GET', '/about', function () { 96 | echo 'about'; 97 | }); 98 | 99 | // Fake some data 100 | $_SERVER['SCRIPT_NAME'] = '/sub/folder/💩/index.php'; 101 | $_SERVER['REQUEST_URI'] = '/sub/folder/%F0%9F%92%A9/about'; 102 | 103 | // Test the /hello/bramus route 104 | ob_start(); 105 | $router->run(); 106 | $this->assertEquals('about', ob_get_contents()); 107 | 108 | // Cleanup 109 | ob_end_clean(); 110 | } 111 | 112 | public function testStaticRoute() 113 | { 114 | // Create Router 115 | $router = new \Bramus\Router\Router(); 116 | $router->match('GET', '/about', function () { 117 | echo 'about'; 118 | }); 119 | 120 | // Test the /about route 121 | ob_start(); 122 | $_SERVER['REQUEST_URI'] = '/about'; 123 | $router->run(); 124 | $this->assertEquals('about', ob_get_contents()); 125 | 126 | // Cleanup 127 | ob_end_clean(); 128 | } 129 | 130 | public function testStaticRouteUsingShorthand() 131 | { 132 | // Create Router 133 | $router = new \Bramus\Router\Router(); 134 | $router->get('/about', function () { 135 | echo 'about'; 136 | }); 137 | 138 | // Test the /about route 139 | ob_start(); 140 | $_SERVER['REQUEST_URI'] = '/about'; 141 | $router->run(); 142 | $this->assertEquals('about', ob_get_contents()); 143 | 144 | // Cleanup 145 | ob_end_clean(); 146 | } 147 | 148 | public function testRequestMethods() 149 | { 150 | // Create Router 151 | $router = new \Bramus\Router\Router(); 152 | $router->get('/', function () { 153 | echo 'get'; 154 | }); 155 | $router->post('/', function () { 156 | echo 'post'; 157 | }); 158 | $router->put('/', function () { 159 | echo 'put'; 160 | }); 161 | $router->patch('/', function () { 162 | echo 'patch'; 163 | }); 164 | $router->delete('/', function () { 165 | echo 'delete'; 166 | }); 167 | $router->options('/', function () { 168 | echo 'options'; 169 | }); 170 | 171 | // Test GET 172 | ob_start(); 173 | $_SERVER['REQUEST_URI'] = '/'; 174 | $router->run(); 175 | $this->assertEquals('get', ob_get_contents()); 176 | 177 | // Test POST 178 | ob_clean(); 179 | $_SERVER['REQUEST_METHOD'] = 'POST'; 180 | $router->run(); 181 | $this->assertEquals('post', ob_get_contents()); 182 | 183 | // Test PUT 184 | ob_clean(); 185 | $_SERVER['REQUEST_METHOD'] = 'PUT'; 186 | $router->run(); 187 | $this->assertEquals('put', ob_get_contents()); 188 | 189 | // Test PATCH 190 | ob_clean(); 191 | $_SERVER['REQUEST_METHOD'] = 'PATCH'; 192 | $router->run(); 193 | $this->assertEquals('patch', ob_get_contents()); 194 | 195 | // Test DELETE 196 | ob_clean(); 197 | $_SERVER['REQUEST_METHOD'] = 'DELETE'; 198 | $router->run(); 199 | $this->assertEquals('delete', ob_get_contents()); 200 | 201 | // Test OPTIONS 202 | ob_clean(); 203 | $_SERVER['REQUEST_METHOD'] = 'OPTIONS'; 204 | $router->run(); 205 | $this->assertEquals('options', ob_get_contents()); 206 | 207 | // Test HEAD 208 | ob_clean(); 209 | $_SERVER['REQUEST_METHOD'] = 'HEAD'; 210 | $router->run(); 211 | $this->assertEquals('', ob_get_contents()); 212 | 213 | // Cleanup 214 | ob_end_clean(); 215 | } 216 | 217 | public function testShorthandAll() 218 | { 219 | // Create Router 220 | $router = new \Bramus\Router\Router(); 221 | $router->all('/', function () { 222 | echo 'all'; 223 | }); 224 | 225 | $_SERVER['REQUEST_URI'] = '/'; 226 | 227 | // Test GET 228 | ob_start(); 229 | $_SERVER['REQUEST_METHOD'] = 'GET'; 230 | $router->run(); 231 | $this->assertEquals('all', ob_get_contents()); 232 | 233 | // Test POST 234 | ob_clean(); 235 | $_SERVER['REQUEST_METHOD'] = 'POST'; 236 | $router->run(); 237 | $this->assertEquals('all', ob_get_contents()); 238 | 239 | // Test PUT 240 | ob_clean(); 241 | $_SERVER['REQUEST_METHOD'] = 'PUT'; 242 | $router->run(); 243 | $this->assertEquals('all', ob_get_contents()); 244 | 245 | // Test DELETE 246 | ob_clean(); 247 | $_SERVER['REQUEST_METHOD'] = 'DELETE'; 248 | $router->run(); 249 | $this->assertEquals('all', ob_get_contents()); 250 | 251 | // Test OPTIONS 252 | ob_clean(); 253 | $_SERVER['REQUEST_METHOD'] = 'OPTIONS'; 254 | $router->run(); 255 | $this->assertEquals('all', ob_get_contents()); 256 | 257 | // Test PATCH 258 | ob_clean(); 259 | $_SERVER['REQUEST_METHOD'] = 'PATCH'; 260 | $router->run(); 261 | $this->assertEquals('all', ob_get_contents()); 262 | 263 | // Test HEAD 264 | ob_clean(); 265 | $_SERVER['REQUEST_METHOD'] = 'HEAD'; 266 | $router->run(); 267 | $this->assertEquals('', ob_get_contents()); 268 | 269 | // Cleanup 270 | ob_end_clean(); 271 | } 272 | 273 | public function testDynamicRoute() 274 | { 275 | // Create Router 276 | $router = new \Bramus\Router\Router(); 277 | $router->get('/hello/(\w+)', function ($name) { 278 | echo 'Hello ' . $name; 279 | }); 280 | 281 | // Test the /hello/bramus route 282 | ob_start(); 283 | $_SERVER['REQUEST_URI'] = '/hello/bramus'; 284 | $router->run(); 285 | $this->assertEquals('Hello bramus', ob_get_contents()); 286 | 287 | // Cleanup 288 | ob_end_clean(); 289 | } 290 | 291 | public function testDynamicRouteWithMultiple() 292 | { 293 | // Create Router 294 | $router = new \Bramus\Router\Router(); 295 | $router->get('/hello/(\w+)/(\w+)', function ($name, $lastname) { 296 | echo 'Hello ' . $name . ' ' . $lastname; 297 | }); 298 | 299 | // Test the /hello/bramus route 300 | ob_start(); 301 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 302 | $router->run(); 303 | $this->assertEquals('Hello bramus sumarb', ob_get_contents()); 304 | 305 | // Cleanup 306 | ob_end_clean(); 307 | } 308 | 309 | public function testCurlyBracesRoutes() 310 | { 311 | // Create Router 312 | $router = new \Bramus\Router\Router(); 313 | $router->get('/hello/{name}/{lastname}', function ($name, $lastname) { 314 | echo 'Hello ' . $name . ' ' . $lastname; 315 | }); 316 | 317 | // Test the /hello/bramus route 318 | ob_start(); 319 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 320 | $router->run(); 321 | $this->assertEquals('Hello bramus sumarb', ob_get_contents()); 322 | 323 | // Cleanup 324 | ob_end_clean(); 325 | } 326 | 327 | public function testCurlyBracesRoutesWithNonAZCharsInPlaceholderNames() 328 | { 329 | // Create Router 330 | $router = new \Bramus\Router\Router(); 331 | $router->get('/hello/{arg1}/{arg2}', function ($arg1, $arg2) { 332 | echo 'Hello ' . $arg1 . ' ' . $arg2; 333 | }); 334 | 335 | // Test the /hello/bramus route 336 | ob_start(); 337 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 338 | $router->run(); 339 | $this->assertEquals('Hello bramus sumarb', ob_get_contents()); 340 | 341 | // Cleanup 342 | ob_end_clean(); 343 | } 344 | 345 | public function testCurlyBracesRoutesWithCyrillicCharactersInPlaceholderNames() 346 | { 347 | // Create Router 348 | $router = new \Bramus\Router\Router(); 349 | $router->get('/hello/{това}/{това}', function ($arg1, $arg2) { 350 | echo 'Hello ' . $arg1 . ' ' . $arg2; 351 | }); 352 | 353 | // Test the /hello/bramus route 354 | ob_start(); 355 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 356 | $router->run(); 357 | $this->assertEquals('Hello bramus sumarb', ob_get_contents()); 358 | 359 | // Cleanup 360 | ob_end_clean(); 361 | } 362 | 363 | public function testCurlyBracesRoutesWithEmojiInPlaceholderNames() 364 | { 365 | // Create Router 366 | $router = new \Bramus\Router\Router(); 367 | $router->get('/hello/{😂}/{😅}', function ($arg1, $arg2) { 368 | echo 'Hello ' . $arg1 . ' ' . $arg2; 369 | }); 370 | 371 | // Test the /hello/bramus route 372 | ob_start(); 373 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 374 | $router->run(); 375 | $this->assertEquals('Hello bramus sumarb', ob_get_contents()); 376 | 377 | // Cleanup 378 | ob_end_clean(); 379 | } 380 | 381 | public function testCurlyBracesWithCyrillicCharacters() 382 | { 383 | // Create Router 384 | $router = new \Bramus\Router\Router(); 385 | $router->get('/bg/{arg}', function ($arg) { 386 | echo 'BG: ' . $arg; 387 | }); 388 | 389 | // Test the /hello/bramus route 390 | ob_start(); 391 | $_SERVER['REQUEST_URI'] = '/bg/това'; 392 | $router->run(); 393 | $this->assertEquals('BG: това', ob_get_contents()); 394 | 395 | // Cleanup 396 | ob_end_clean(); 397 | } 398 | 399 | public function testCurlyBracesWithMultipleCyrillicCharacters() 400 | { 401 | // Create Router 402 | $router = new \Bramus\Router\Router(); 403 | $router->get('/bg/{arg}/{arg}', function ($arg1, $arg2) { 404 | echo 'BG: ' . $arg1 . ' - ' . $arg2; 405 | }); 406 | 407 | // Test the /hello/bramus route 408 | ob_start(); 409 | $_SERVER['REQUEST_URI'] = '/bg/това/слъг'; 410 | $router->run(); 411 | $this->assertEquals('BG: това - слъг', ob_get_contents()); 412 | 413 | // Cleanup 414 | ob_end_clean(); 415 | } 416 | 417 | public function testCurlyBracesWithEmoji() 418 | { 419 | // Create Router 420 | $router = new \Bramus\Router\Router(); 421 | $router->get('/emoji/{emoji}', function ($emoji) { 422 | echo 'Emoji: ' . $emoji; 423 | }); 424 | 425 | // Test the /hello/bramus route 426 | ob_start(); 427 | $_SERVER['REQUEST_URI'] = '/emoji/%F0%9F%92%A9'; // 💩 428 | $router->run(); 429 | $this->assertEquals('Emoji: 💩', ob_get_contents()); 430 | 431 | // Cleanup 432 | ob_end_clean(); 433 | } 434 | 435 | public function testCurlyBracesWithEmojiCombinedWithBasePathThatContainsEmoji() 436 | { 437 | // Create Router 438 | $router = new \Bramus\Router\Router(); 439 | $router->get('/emoji/{emoji}', function ($emoji) { 440 | echo 'Emoji: ' . $emoji; 441 | }); 442 | 443 | // Fake some data 444 | $_SERVER['SCRIPT_NAME'] = '/sub/folder/💩/index.php'; 445 | $_SERVER['REQUEST_URI'] = '/sub/folder/%F0%9F%92%A9/emoji/%F0%9F%A4%AF'; // 🤯 446 | 447 | // Test the /hello/bramus route 448 | ob_start(); 449 | $router->run(); 450 | $this->assertEquals('Emoji: 🤯', ob_get_contents()); 451 | 452 | // Cleanup 453 | ob_end_clean(); 454 | } 455 | 456 | public function testDynamicRouteWithOptionalSubpatterns() 457 | { 458 | // Create Router 459 | $router = new \Bramus\Router\Router(); 460 | $router->get('/hello(/\w+)?', function ($name = null) { 461 | echo 'Hello ' . (($name) ? $name : 'stranger'); 462 | }); 463 | 464 | // Test the /hello route 465 | ob_start(); 466 | $_SERVER['REQUEST_URI'] = '/hello'; 467 | $router->run(); 468 | $this->assertEquals('Hello stranger', ob_get_contents()); 469 | 470 | // Test the /hello/bramus route 471 | ob_clean(); 472 | $_SERVER['REQUEST_URI'] = '/hello/bramus'; 473 | $router->run(); 474 | $this->assertEquals('Hello bramus', ob_get_contents()); 475 | 476 | // Cleanup 477 | ob_end_clean(); 478 | } 479 | 480 | public function testDynamicRouteWithMultipleSubpatterns() 481 | { 482 | // Create Router 483 | $router = new \Bramus\Router\Router(); 484 | $router->get('/(.*)/page([0-9]+)', function ($place, $page) { 485 | echo 'Hello ' . $place . ' page : ' . $page; 486 | }); 487 | 488 | // Test the /hello/bramus/page3 route 489 | ob_start(); 490 | $_SERVER['REQUEST_URI'] = '/hello/bramus/page3'; 491 | $router->run(); 492 | $this->assertEquals('Hello hello/bramus page : 3', ob_get_contents()); 493 | 494 | // Cleanup 495 | ob_end_clean(); 496 | } 497 | 498 | public function testDynamicRouteWithOptionalNestedSubpatterns() 499 | { 500 | // Create Router 501 | $router = new \Bramus\Router\Router(); 502 | $router->get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function ($year = null, $month = null, $day = null, $slug = null) { 503 | if ($year === null) { 504 | echo 'Blog overview'; 505 | 506 | return; 507 | } 508 | if ($month === null) { 509 | echo 'Blog year overview (' . $year . ')'; 510 | 511 | return; 512 | } 513 | if ($day === null) { 514 | echo 'Blog month overview (' . $year . '-' . $month . ')'; 515 | 516 | return; 517 | } 518 | if ($slug === null) { 519 | echo 'Blog day overview (' . $year . '-' . $month . '-' . $day . ')'; 520 | 521 | return; 522 | } 523 | echo 'Blogpost ' . htmlentities($slug) . ' detail (' . $year . '-' . $month . '-' . $day . ')'; 524 | }); 525 | 526 | // Test the /blog route 527 | ob_start(); 528 | $_SERVER['REQUEST_URI'] = '/blog'; 529 | $router->run(); 530 | $this->assertEquals('Blog overview', ob_get_contents()); 531 | 532 | // Test the /blog/year route 533 | ob_clean(); 534 | $_SERVER['REQUEST_URI'] = '/blog/1983'; 535 | $router->run(); 536 | $this->assertEquals('Blog year overview (1983)', ob_get_contents()); 537 | 538 | // Test the /blog/year/month route 539 | ob_clean(); 540 | $_SERVER['REQUEST_URI'] = '/blog/1983/12'; 541 | $router->run(); 542 | $this->assertEquals('Blog month overview (1983-12)', ob_get_contents()); 543 | 544 | // Test the /blog/year/month/day route 545 | ob_clean(); 546 | $_SERVER['REQUEST_URI'] = '/blog/1983/12/26'; 547 | $router->run(); 548 | $this->assertEquals('Blog day overview (1983-12-26)', ob_get_contents()); 549 | 550 | // Test the /blog/year/month/day/slug route 551 | ob_clean(); 552 | $_SERVER['REQUEST_URI'] = '/blog/1983/12/26/bramus'; 553 | $router->run(); 554 | $this->assertEquals('Blogpost bramus detail (1983-12-26)', ob_get_contents()); 555 | 556 | // Cleanup 557 | ob_end_clean(); 558 | } 559 | 560 | public function testDynamicRouteWithNestedOptionalSubpatterns() 561 | { 562 | // Create Router 563 | $router = new \Bramus\Router\Router(); 564 | $router->get('/hello(/\w+(/\w+)?)?', function ($name1 = null, $name2 = null) { 565 | echo 'Hello ' . (($name1) ? $name1 : 'stranger') . ' ' . (($name2) ? $name2 : 'stranger'); 566 | }); 567 | 568 | // Test the /hello/bramus route 569 | ob_start(); 570 | $_SERVER['REQUEST_URI'] = '/hello/bramus'; 571 | $router->run(); 572 | $this->assertEquals('Hello bramus stranger', ob_get_contents()); 573 | 574 | // Test the /hello/bramus/bramus route 575 | ob_clean(); 576 | $_SERVER['REQUEST_URI'] = '/hello/bramus/bramus'; 577 | $router->run(); 578 | $this->assertEquals('Hello bramus bramus', ob_get_contents()); 579 | 580 | // Cleanup 581 | ob_end_clean(); 582 | } 583 | 584 | public function testDynamicRouteWithWildcard() 585 | { 586 | // Create Router 587 | $router = new \Bramus\Router\Router(); 588 | $router->get('(.*)', function ($name) { 589 | echo 'Hello ' . $name; 590 | }); 591 | 592 | // Test the /hello/bramus route 593 | ob_start(); 594 | $_SERVER['REQUEST_URI'] = '/hello/bramus'; 595 | $router->run(); 596 | $this->assertEquals('Hello hello/bramus', ob_get_contents()); 597 | 598 | // Cleanup 599 | ob_end_clean(); 600 | } 601 | 602 | public function testDynamicRouteWithPartialWildcard() 603 | { 604 | // Create Router 605 | $router = new \Bramus\Router\Router(); 606 | $router->get('/hello/(.*)', function ($name) { 607 | echo 'Hello ' . $name; 608 | }); 609 | 610 | // Test the /hello/bramus route 611 | ob_start(); 612 | $_SERVER['REQUEST_URI'] = '/hello/bramus/sumarb'; 613 | $router->run(); 614 | $this->assertEquals('Hello bramus/sumarb', ob_get_contents()); 615 | 616 | // Cleanup 617 | ob_end_clean(); 618 | } 619 | 620 | public function test404() 621 | { 622 | // Create Router 623 | $router = new \Bramus\Router\Router(); 624 | $router->get('/', function () { 625 | echo 'home'; 626 | }); 627 | $router->set404(function () { 628 | echo 'route not found'; 629 | }); 630 | 631 | $router->set404('/api(/.*)?', function () { 632 | echo 'api route not found'; 633 | }); 634 | 635 | // Test the /hello route 636 | ob_start(); 637 | $_SERVER['REQUEST_URI'] = '/'; 638 | $router->run(); 639 | $this->assertEquals('home', ob_get_contents()); 640 | 641 | // Test the /hello/bramus route 642 | ob_clean(); 643 | $_SERVER['REQUEST_URI'] = '/foo'; 644 | $router->run(); 645 | $this->assertEquals('route not found', ob_get_contents()); 646 | 647 | // Test the custom api 404 648 | ob_clean(); 649 | $_SERVER['REQUEST_URI'] = '/api/getUser'; 650 | $router->run(); 651 | $this->assertEquals('api route not found', ob_get_contents()); 652 | 653 | // Cleanup 654 | ob_end_clean(); 655 | } 656 | 657 | public function test404WithClassAtMethod() 658 | { 659 | // Create Router 660 | $router = new \Bramus\Router\Router(); 661 | $router->get('/', function () { 662 | echo 'home'; 663 | }); 664 | 665 | $router->set404('Handler@notFound'); 666 | 667 | // Test the /hello route 668 | ob_start(); 669 | $_SERVER['REQUEST_URI'] = '/'; 670 | $router->run(); 671 | $this->assertEquals('home', ob_get_contents()); 672 | 673 | // Test the /hello/bramus route 674 | ob_clean(); 675 | $_SERVER['REQUEST_URI'] = '/foo'; 676 | $router->run(); 677 | $this->assertEquals('route not found', ob_get_contents()); 678 | 679 | // Cleanup 680 | ob_end_clean(); 681 | } 682 | 683 | public function test404WithClassAtStaticMethod() 684 | { 685 | // Create Router 686 | $router = new \Bramus\Router\Router(); 687 | $router->get('/', function () { 688 | echo 'home'; 689 | }); 690 | 691 | $router->set404('Handler@notFound'); 692 | 693 | // Test the /hello route 694 | ob_start(); 695 | $_SERVER['REQUEST_URI'] = '/'; 696 | $router->run(); 697 | $this->assertEquals('home', ob_get_contents()); 698 | 699 | // Test the /hello/bramus route 700 | ob_clean(); 701 | $_SERVER['REQUEST_URI'] = '/foo'; 702 | $router->run(); 703 | $this->assertEquals('route not found', ob_get_contents()); 704 | 705 | // Cleanup 706 | ob_end_clean(); 707 | } 708 | 709 | public function test404WithManualTrigger() 710 | { 711 | // Create Router 712 | $router = new \Bramus\Router\Router(); 713 | $router->get('/', function() use ($router) { 714 | $router->trigger404(); 715 | }); 716 | $router->set404(function () { 717 | echo 'route not found'; 718 | }); 719 | 720 | // Test the / route 721 | ob_start(); 722 | $_SERVER['REQUEST_URI'] = '/'; 723 | $router->run(); 724 | $this->assertEquals('route not found', ob_get_contents()); 725 | 726 | // Cleanup 727 | ob_end_clean(); 728 | } 729 | 730 | public function testBeforeRouterMiddleware() 731 | { 732 | // Create Router 733 | $router = new \Bramus\Router\Router(); 734 | $router->before('GET|POST', '/.*', function () { 735 | echo 'before '; 736 | }); 737 | $router->get('/', function () { 738 | echo 'root'; 739 | }); 740 | $router->get('/about', function () { 741 | echo 'about'; 742 | }); 743 | $router->get('/contact', function () { 744 | echo 'contact'; 745 | }); 746 | $router->post('/post', function () { 747 | echo 'post'; 748 | }); 749 | 750 | // Test the / route 751 | ob_start(); 752 | $_SERVER['REQUEST_URI'] = '/'; 753 | $router->run(); 754 | $this->assertEquals('before root', ob_get_contents()); 755 | 756 | // Test the /about route 757 | ob_clean(); 758 | $_SERVER['REQUEST_URI'] = '/about'; 759 | $router->run(); 760 | $this->assertEquals('before about', ob_get_contents()); 761 | 762 | // Test the /contact route 763 | ob_clean(); 764 | $_SERVER['REQUEST_URI'] = '/contact'; 765 | $router->run(); 766 | $this->assertEquals('before contact', ob_get_contents()); 767 | 768 | // Test the /post route 769 | ob_clean(); 770 | $_SERVER['REQUEST_URI'] = '/post'; 771 | $_SERVER['REQUEST_METHOD'] = 'POST'; 772 | $router->run(); 773 | $this->assertEquals('before post', ob_get_contents()); 774 | 775 | // Cleanup 776 | ob_end_clean(); 777 | } 778 | 779 | public function testAfterRouterMiddleware() 780 | { 781 | // Create Router 782 | $router = new \Bramus\Router\Router(); 783 | $router->get('/', function () { 784 | echo 'home'; 785 | }); 786 | 787 | // Test the / route 788 | ob_start(); 789 | $_SERVER['REQUEST_URI'] = '/'; 790 | $router->run(function () { 791 | echo 'finished'; 792 | }); 793 | $this->assertEquals('homefinished', ob_get_contents()); 794 | 795 | // Cleanup 796 | ob_end_clean(); 797 | } 798 | 799 | public function testBasicController() 800 | { 801 | $router = new \Bramus\Router\Router(); 802 | 803 | $router->get('/show/(.*)', 'RouterTestController@show'); 804 | 805 | ob_start(); 806 | $_SERVER['REQUEST_URI'] = '/show/foo'; 807 | $router->run(); 808 | 809 | $this->assertEquals('foo', ob_get_contents()); 810 | 811 | // cleanup 812 | ob_end_clean(); 813 | } 814 | 815 | public function testDefaultNamespace() 816 | { 817 | $router = new \Bramus\Router\Router(); 818 | 819 | $router->setNamespace('\Hello'); 820 | 821 | $router->get('/show/(.*)', 'HelloRouterTestController@show'); 822 | 823 | ob_start(); 824 | $_SERVER['REQUEST_URI'] = '/show/foo'; 825 | $router->run(); 826 | 827 | $this->assertEquals('foo', ob_get_contents()); 828 | 829 | // cleanup 830 | ob_end_clean(); 831 | } 832 | 833 | public function testSubfolders() 834 | { 835 | // Create Router 836 | $router = new \Bramus\Router\Router(); 837 | $router->get('/', function () { 838 | echo 'home'; 839 | }); 840 | 841 | // Test the / route in a fake subfolder 842 | ob_start(); 843 | $_SERVER['SCRIPT_NAME'] = '/about/index.php'; 844 | $_SERVER['REQUEST_URI'] = '/about/'; 845 | $router->run(); 846 | $this->assertEquals('home', ob_get_contents()); 847 | 848 | // Cleanup 849 | ob_end_clean(); 850 | } 851 | 852 | public function testSubrouteMouting() 853 | { 854 | // Create Router 855 | $router = new \Bramus\Router\Router(); 856 | $router->mount('/movies', function () use ($router) { 857 | $router->get('/', function () { 858 | echo 'overview'; 859 | }); 860 | $router->get('/(\d+)', function ($id) { 861 | echo htmlentities($id); 862 | }); 863 | }); 864 | 865 | // Test the /movies route 866 | ob_start(); 867 | $_SERVER['REQUEST_URI'] = '/movies'; 868 | $router->run(); 869 | $this->assertEquals('overview', ob_get_contents()); 870 | 871 | // Test the /hello/bramus route 872 | ob_clean(); 873 | $_SERVER['REQUEST_URI'] = '/movies/1'; 874 | $router->run(); 875 | $this->assertEquals('1', ob_get_contents()); 876 | 877 | // Cleanup 878 | ob_end_clean(); 879 | } 880 | 881 | public function testHttpMethodOverride() 882 | { 883 | // Fake the request method to being POST and override it 884 | $_SERVER['REQUEST_METHOD'] = 'POST'; 885 | $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT'; 886 | 887 | $method = new ReflectionMethod( 888 | '\Bramus\Router\Router', 889 | 'getRequestMethod' 890 | ); 891 | 892 | $method->setAccessible(true); 893 | 894 | $this->assertEquals( 895 | 'PUT', 896 | $method->invoke(new \Bramus\Router\Router()) 897 | ); 898 | } 899 | 900 | public function testControllerMethodReturningFalse() 901 | { 902 | // Create Router 903 | $router = new \Bramus\Router\Router(); 904 | $router->get('/false', 'RouterTestController@returnFalse'); 905 | $router->get('/static-false', 'RouterTestController@staticReturnFalse'); 906 | 907 | // Test returnFalse 908 | ob_start(); 909 | $_SERVER['REQUEST_URI'] = '/false'; 910 | $router->run(); 911 | $this->assertEquals('returnFalse', ob_get_contents()); 912 | 913 | // Test staticReturnFalse 914 | ob_clean(); 915 | $_SERVER['REQUEST_URI'] = '/static-false'; 916 | $router->run(); 917 | $this->assertEquals('staticReturnFalse', ob_get_contents()); 918 | 919 | // Cleanup 920 | ob_end_clean(); 921 | } 922 | } 923 | } 924 | 925 | namespace { 926 | class RouterTestController 927 | { 928 | public function show($id) 929 | { 930 | echo $id; 931 | } 932 | 933 | public function returnFalse() 934 | { 935 | echo 'returnFalse'; 936 | 937 | return false; 938 | } 939 | 940 | public static function staticReturnFalse() 941 | { 942 | echo 'staticReturnFalse'; 943 | 944 | return false; 945 | } 946 | } 947 | } 948 | 949 | namespace Hello { 950 | class HelloRouterTestController 951 | { 952 | public function show($id) 953 | { 954 | echo $id; 955 | } 956 | } 957 | } 958 | 959 | // EOF 960 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |