|
| 1 | +# CommandString/Router |
| 2 | + |
| 3 | +A router package built that uses ReactPHP/HTTP under the hood |
| 4 | + |
| 5 | +# Table of Contents |
| 6 | + |
| 7 | +- [Getting Started](#getting-started) |
| 8 | +- [Creating routes](#routing) |
| 9 | +- [Patterns](#route-patterns) |
| 10 | +- [Controllers](#controllers) |
| 11 | +- [Middleware](#middleware) |
| 12 | +- [Dev Mode](#dev-mode) |
| 13 | +- [Nodemon](#nodemon) |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +``` |
| 18 | +composer require commandstring/router |
| 19 | +``` |
| 20 | + |
| 21 | +## Getting started |
| 22 | +You first need to create a ReactPHP SocketServer |
| 23 | + |
| 24 | +```php |
| 25 | +$socket = new \React\Socket\SocketServer("{$env->server->ip}:{$env->server->port}"); |
| 26 | +``` |
| 27 | + |
| 28 | +Then create a router instance |
| 29 | + |
| 30 | +```php |
| 31 | +$router = new \Router\Http\Router($socket, true); |
| 32 | +``` |
| 33 | + |
| 34 | +The second parameter is whether dev mode should be enabled or not you can read about dev mode [here](#dev-mode) |
| 35 | + |
| 36 | +## Routing |
| 37 | + |
| 38 | +You can then add routes by using the match method |
| 39 | + |
| 40 | +```php |
| 41 | +use Router\Http\Methods; |
| 42 | + |
| 43 | +$router->match([Methods::GET], "/", function() { /* ... */ }); |
| 44 | +``` |
| 45 | + |
| 46 | +You can listen for more methods by adding them to the array |
| 47 | + |
| 48 | +```php |
| 49 | +$router->match([Methods::GET, Methods::POST], "/", function() { /* ... */ }); |
| 50 | +``` |
| 51 | + |
| 52 | +## Routing Shorthands |
| 53 | + |
| 54 | +Shorthands for single request methods are provided |
| 55 | + |
| 56 | +```php |
| 57 | +$router->get('pattern', function() { /* ... */ }); |
| 58 | +$router->post('pattern', function() { /* ... */ }); |
| 59 | +$router->put('pattern', function() { /* ... */ }); |
| 60 | +$router->delete('pattern', function() { /* ... */ }); |
| 61 | +$router->options('pattern', function() { /* ... */ }); |
| 62 | +$router->patch('pattern', function() { /* ... */ }); |
| 63 | +``` |
| 64 | + |
| 65 | +You can use this shorthand for a route that can be accessed using any method: |
| 66 | + |
| 67 | +```php |
| 68 | +$router->all('pattern', function() { /* ... */ }); |
| 69 | +``` |
| 70 | + |
| 71 | +# Route Patterns |
| 72 | + |
| 73 | +Route Patterns can be static or dynamic: |
| 74 | + |
| 75 | +- __Static Route Patterns__ contain no dynamic parts and must match exactly against the `path` part of the current URL. |
| 76 | +- __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__ |
| 77 | + |
| 78 | +## Static Route Patterns |
| 79 | + |
| 80 | +A static route pattern is a regular string representing a URI. It will be compared directly against the `path` part of the current URL. |
| 81 | + |
| 82 | +Examples: |
| 83 | + |
| 84 | +- `/about` |
| 85 | +- `/contact` |
| 86 | + |
| 87 | +Usage Examples: |
| 88 | + |
| 89 | +```php |
| 90 | +$router->get('/about', function($req, $res) { |
| 91 | + $res->getBody()->write("Hello World"); |
| 92 | + return $res; |
| 93 | +}); |
| 94 | +``` |
| 95 | + |
| 96 | +## Dynamic PCRE-based Route Patterns |
| 97 | + |
| 98 | +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. |
| 99 | + |
| 100 | +Examples: |
| 101 | + |
| 102 | +- `/movies/(\d+)` |
| 103 | +- `/profile/(\w+)` |
| 104 | + |
| 105 | +Commonly used PCRE-based subpatterns within Dynamic Route Patterns are: |
| 106 | + |
| 107 | +- `\d+` = One or more digits (0-9) |
| 108 | +- `\w+` = One or more word characters (a-z 0-9 _) |
| 109 | +- `[a-z0-9_-]+` = One or more word characters (a-z 0-9 _) and the dash (-) |
| 110 | +- `.*` = Any character (including `/`), zero or more |
| 111 | +- `[^/]+` = Any character but `/`, one or more |
| 112 | + |
| 113 | +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. |
| 114 | + |
| 115 | +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: |
| 116 | + |
| 117 | +```php |
| 118 | +// Bad |
| 119 | +$router->get('/hello/\w+', function($req, $res, $name) { |
| 120 | + $res->getBody()->write('Hello '.htmlentities($name)); |
| 121 | + return $res; |
| 122 | +}); |
| 123 | + |
| 124 | +// Good |
| 125 | +$router->get('/hello/(\w+)', function($req, $res, $name) { |
| 126 | + $res->getBody()->write('Hello '.htmlentities($name)); |
| 127 | + return $res; |
| 128 | +}); |
| 129 | +``` |
| 130 | + |
| 131 | +Note: The leading `/` at the very beginning of a route pattern is not mandatory, but is recommended. |
| 132 | + |
| 133 | +When multiple subpatterns are defined, the resulting __route handling parameters__ are passed into the route handling function in the order they are defined in: |
| 134 | + |
| 135 | +```php |
| 136 | +$router->get('/movies/(\d+)/photos/(\d+)', function($res, $movieId, $photoId) { |
| 137 | + $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId); |
| 138 | + return $res; |
| 139 | +}); |
| 140 | +``` |
| 141 | + |
| 142 | +## Dynamic Placeholder-based Route Patterns |
| 143 | + |
| 144 | +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. |
| 145 | + |
| 146 | +Examples: |
| 147 | + |
| 148 | +- `/movies/{id}` |
| 149 | +- `/profile/{username}` |
| 150 | + |
| 151 | +Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (`.*`). |
| 152 | + |
| 153 | +```php |
| 154 | +$router->get('/movies/{movieId}/photos/{photoId}', function($req, $res, $movieId, $photoId) { |
| 155 | + $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId); |
| 156 | + return $res; |
| 157 | +}); |
| 158 | +``` |
| 159 | + |
| 160 | +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: |
| 161 | + |
| 162 | +```php |
| 163 | +$router->get('/movies/{foo}/photos/{bar}', function($req, $res, $movieId, $photoId) { |
| 164 | + $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId); |
| 165 | + return $res; |
| 166 | +}); |
| 167 | +``` |
| 168 | + |
| 169 | +### Optional Route Subpatterns |
| 170 | + |
| 171 | +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)`: |
| 172 | + |
| 173 | +```php |
| 174 | +$router->get( |
| 175 | + '/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?', |
| 176 | + function($req, $res, $year = null, $month = null, $day = null, $slug = null) { |
| 177 | + if (!$year) { |
| 178 | + $res->getBody()->write("Blog Overview"); |
| 179 | + return $res; |
| 180 | + } |
| 181 | + |
| 182 | + if (!$month) { |
| 183 | + $res->getBody()->write("Blog year overview"); |
| 184 | + return $res; |
| 185 | + } |
| 186 | + |
| 187 | + if (!$day) { |
| 188 | + $res->getBody()->write("Blog month overview"); |
| 189 | + return $res; |
| 190 | + } |
| 191 | + |
| 192 | + if (!$slug) { |
| 193 | + $res->getBody()->write("Blog day overview"); |
| 194 | + return $res; |
| 195 | + } |
| 196 | + |
| 197 | + $res->getBody()->write('Blogpost ' . htmlentities($slug) . ' detail'); |
| 198 | + return $res; |
| 199 | + } |
| 200 | +); |
| 201 | +``` |
| 202 | + |
| 203 | +The code snippet above responds to the URLs `/blog`, `/blog/year`, `/blog/year/month`, `/blog/year/month/day`, and `/blog/year/month/day/slug`. |
| 204 | + |
| 205 | +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. |
| 206 | + |
| 207 | +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)`: |
| 208 | +```php |
| 209 | +$router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) { |
| 210 | + // ... |
| 211 | +}); |
| 212 | +``` |
| 213 | + |
| 214 | +Note: It is highly recommended to __always__ define successive optional parameters. |
| 215 | + |
| 216 | +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: |
| 217 | + |
| 218 | +```php |
| 219 | +$router->get('/blog(/\d{4}(/\d{2}(/\d{2zz}(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) { |
| 220 | + // ... |
| 221 | +}); |
| 222 | +``` |
| 223 | + |
| 224 | +# Controllers |
| 225 | + |
| 226 | +When defining a route you can either pass an anonymous function or an array that contains a class along with a static method to invoke. Additionally your controller must return an implementation of the PSR7 Response Interface |
| 227 | + |
| 228 | +## Anonymous Function Controller |
| 229 | + |
| 230 | +```php |
| 231 | +$router->get("/home", function ($req, $res) { |
| 232 | + $res->getBody()->write("Welcome home!"); |
| 233 | + return $res; |
| 234 | +}); |
| 235 | +``` |
| 236 | + |
| 237 | +## Class Controller |
| 238 | + |
| 239 | +I have a class with a **static** method, your handler MUST be a static method! |
| 240 | + |
| 241 | +```php |
| 242 | +class Home { |
| 243 | + public static function handler($req, $res) { |
| 244 | + $res->getBody()->write("Welcome home!"); |
| 245 | + return $res; |
| 246 | + } |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +I then replace the anonymous function with an array the first item being the class string and the second key being the name of the static method. |
| 251 | + |
| 252 | +```php |
| 253 | +$router->get("/home", [Home::class, "handler"]); |
| 254 | +``` |
| 255 | + |
| 256 | +## 404 Handler |
| 257 | + |
| 258 | +Defining a 404 handler is **required** and is similar to creating a route. You can also have different 404 pages for different patterns. |
| 259 | + |
| 260 | +To setup a 404 handler you can invoke the map404 method and insert a pattern for the first parameter and then your controller as the second. |
| 261 | + |
| 262 | +```php |
| 263 | +$router->map404("/(.*)", function ($req, $res) { |
| 264 | + $res->getBody()->write("{$req->getRequestTarget()} is not a valid route"); |
| 265 | + return $res; |
| 266 | +}); |
| 267 | +``` |
| 268 | + |
| 269 | +## 500 handler |
| 270 | + |
| 271 | +Defining a 500 handler is **recommended** and is exactly same to mapping a 404 handler. |
| 272 | + |
| 273 | +```php |
| 274 | +$router->map500("/(.*)", function ($req, $res) { |
| 275 | + $res->getBody()->write("An error has happened internally :("); |
| 276 | + return $res; |
| 277 | +}); |
| 278 | +``` |
| 279 | +*Note that when in development mode your 500 error handler will be overrode* |
| 280 | + |
| 281 | +# Middleware |
| 282 | + |
| 283 | +Middleware is software that connects the model and view in an MVC application, facilitating the communication and data flow between these two components while also providing a layer of abstraction, decoupling the model and view and allowing them to interact without needing to know the details of how the other component operates. |
| 284 | + |
| 285 | +A good example is having before middleware that makes sure the user is an administrator before they go to a restricted page. You could do this in your routes controller for every admin page but that would be redundant. Or for after middleware in a REST API that returns JSONs you might want to make sure the body isn't a malformed JSON. |
| 286 | + |
| 287 | +## Before Middleware |
| 288 | + |
| 289 | +You can define before middleware similar to a route by providing a method, pattern, and controller. |
| 290 | + |
| 291 | +```php |
| 292 | +use HttpSoft\Response\RedirectResponse; |
| 293 | + |
| 294 | +$router->beforeMiddleware([METHOD::ALL], "/admin?(.*)", function ($req, $res) { |
| 295 | + if (!isAdmin()) { |
| 296 | + return new RedirectResponse("/", 403); |
| 297 | + } |
| 298 | + |
| 299 | + return $res; |
| 300 | +}); |
| 301 | +``` |
| 302 | + |
| 303 | +## After Middleware |
| 304 | + |
| 305 | +The only difference between defining after and before middleware is the method you use. |
| 306 | + |
| 307 | +```php |
| 308 | +use HttpSoft\Response\RedirectResponse; |
| 309 | + |
| 310 | +$router->afterMiddleware([METHOD::ALL], "/admin?(.*)", function ($req, $res) { |
| 311 | + if (!isAdmin()) { |
| 312 | + return new RedirectResponse("/", 403); |
| 313 | + } |
| 314 | + |
| 315 | + return $res; |
| 316 | +}); |
| 317 | +``` |
| 318 | + |
| 319 | +# Template Engine Integration |
| 320 | + |
| 321 | +You can use [CommandString/Env](https://github.com/commandstring/env) to store your template engine object in a singleton. Then you can easily get it without trying to pass it around to your controller |
| 322 | + |
| 323 | +```php |
| 324 | +use CommandString\Env\Env; |
| 325 | + |
| 326 | +$env = new Env; |
| 327 | +$env->twig = new Environment(new \Twig\Loader\FilesystemLoader("/path/to/views")); |
| 328 | + |
| 329 | +// ... |
| 330 | + |
| 331 | +$router->get("/home", function ($req, $res) { |
| 332 | + return new HtmlResponse($env->get("twig")->render("home.html"));\\\ |
| 333 | +}); |
| 334 | +``` |
| 335 | + |
| 336 | +# Responding to requests |
| 337 | +All response handlers **MUST** return an instance of `\Psr\Http\Message\ResponseInterface`. You can use the `$response` object passed into each handler *or* instantiate your own. I recommend taking a look at [HttpSoft/Response](https://httpsoft.org/docs/response/v1/#usage) for prebuilt response types. This is also included with the route as it's used for the dev mode |
| 338 | +```php |
| 339 | +$response = new HttpSoft\Response\HtmlResponse('<p>HTML</p>'); |
| 340 | +$response = new HttpSoft\Response\JsonResponse(['key' => 'value']); |
| 341 | +$response = new HttpSoft\Response\JsonResponse("{key: 'value'}"); |
| 342 | +$response = new HttpSoft\Response\TextResponse('Text'); |
| 343 | +$response = new HttpSoft\Response\XmlResponse('<xmltag>XML</xmltag>'); |
| 344 | +$response = new HttpSoft\Response\RedirectResponse('https/example.com'); |
| 345 | +$response = new HttpSoft\Response\EmptyResponse(); |
| 346 | +``` |
| 347 | + |
| 348 | +# Dev Mode |
| 349 | + |
| 350 | +As of now dev mode does one thing. When an exception is thrown on your route it returns the exception with the stack trace as a response rather than dumping it into the console. |
| 351 | + |
| 352 | +# Nodemon |
| 353 | +I would recommend using nodemon when developing as it will restart your server with every file change. To install nodemon you'll need nodejs and npm. |
| 354 | + |
| 355 | +`npm install -g nodemon` |
| 356 | + |
| 357 | +then in root of your project directory create a new file named nodemon.json and put the following contents into it |
| 358 | +```json |
| 359 | +{ |
| 360 | + "verbose": false, |
| 361 | + "ignore": [ |
| 362 | + ".git", |
| 363 | + ".idea" |
| 364 | + ], |
| 365 | + "execMap": { |
| 366 | + "php": "php" |
| 367 | + }, |
| 368 | + "restartable": "r", |
| 369 | + "ext": "php,html,json" |
| 370 | +} |
| 371 | +``` |
| 372 | + |
| 373 | +Afterwards instead of using `php index.php` to start your server use `nodemon index.php` and change a file. You'll see that it says the server is restarting due to a file change. And now you don't have to repeatedly restart the server when you change files! You can also enter r into the console to restart manually if needed! |
0 commit comments