Skip to content
This repository was archived by the owner on May 23, 2023. It is now read-only.

Commit a4b500e

Browse files
committed
init
0 parents  commit a4b500e

File tree

11 files changed

+2392
-0
lines changed

11 files changed

+2392
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/

README.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
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!

composer.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"require": {
3+
"react/http": "^1.8.0",
4+
"twig/twig": "^3.4",
5+
"commandstring/env": "^1.0",
6+
"httpsoft/http-response": "^1.0"
7+
},
8+
"autoload": {
9+
"psr-4": {
10+
"Router\\Http\\": "./src"
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)