├── .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 | [](https://github.com/bramus/router/actions) [](https://github.com/bramus/router) [](https://packagist.org/packages/bramus/router) [](https://packagist.org/packages/bramus/router/stats) [](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 |