alexmorbo/reactphp-router

dev-master 2023-02-08 10:18 UTC

This package is auto-updated.

Last update: 2024-04-11 11:56:23 UTC


README

A router package built that uses ReactPHP/HTTP under the hood

Table of Contents

Installation

composer require commandstring/router

Getting started

You first need to create a ReactPHP SocketServer

$socket = new \React\Socket\SocketServer("127.0.0.1:8000");

Then create a router instance

$router = new \Router\Http\Router($socket, true);

The second parameter is whether dev mode should be enabled or not you can read about dev mode here

create some routes then begin listening for requests

$router->listen();

Routing

You can then add routes by using the match method

use Router\Http\Methods;

$router->match([Methods::GET], "/", function() { /* ... */ });

You can listen for more methods by adding them to the array

$router->match([Methods::GET, Methods::POST], "/", function() { /* ... */ });

Routing Shorthands

Shorthands for single request methods are provided

$router->get('pattern', function() { /* ... */ });
$router->post('pattern', function() { /* ... */ });
$router->put('pattern', function() { /* ... */ });
$router->delete('pattern', function() { /* ... */ });
$router->options('pattern', function() { /* ... */ });
$router->patch('pattern', function() { /* ... */ });
$router->head('pattern', function() { /* ... */ });

You can use this shorthand for a route that can be accessed using any method:

$router->all('pattern', function() { /* ... */ });

Route Patterns

Route Patterns can be static or dynamic:

  • Static Route Patterns contain no dynamic parts and must match exactly against the path part of the current URL.
  • 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

Static Route Patterns

A static route pattern is a regular string representing a URI. It will be compared directly against the path part of the current URL.

Examples:

  • /about
  • /contact

Usage Examples:

$router->get('/about', function($req, $res) {
    $res->getBody()->write("Hello World");
    return $res;
});

Dynamic PCRE-based Route Patterns

This type of Route Pattern contains dynamic parts which can vary per request. The varying parts are named subpatterns and are defined using regular expressions.

Examples:

  • /movies/(\d+)
  • /profile/(\w+)

Commonly used PCRE-based subpatterns within Dynamic Route Patterns are:

  • \d+ = One or more digits (0-9)
  • \w+ = One or more word characters (a-z 0-9 _)
  • [a-z0-9_-]+ = One or more word characters (a-z 0-9 _) and the dash (-)
  • .* = Any character (including /), zero or more
  • [^/]+ = Any character but /, one or more

Note: The PHP PCRE Cheat Sheet might come in handy.

The subpatterns defined in Dynamic PCRE-based Route Patterns are converted to parameters that are passed into the route handling function. The prerequisite is that these subpatterns need to be defined as parenthesized subpatterns, which means that they should be wrapped between parenthesis:

// Bad
$router->get('/hello/\w+', function($req, $res, $name) {
    $res->getBody()->write('Hello '.htmlentities($name));
    return $res;
});

// Good
$router->get('/hello/(\w+)', function($req, $res, $name) {
    $res->getBody()->write('Hello '.htmlentities($name));
    return $res;
});

Note: The leading / at the very beginning of a route pattern is not mandatory, but is recommended.

When multiple subpatterns are defined, the resulting route handling parameters are passed into the route handling function in the order they are defined:

$router->get('/movies/(\d+)/photos/(\d+)', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

Dynamic Placeholder-based Route Patterns

This type of Route Pattern is 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.

Examples:

  • /movies/{id}
  • /profile/{username}

Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (.*).

$router->get('/movies/{movieId}/photos/{photoId}', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

Note: the name of the placeholder does not need to match the name of the parameter that is passed into the route handling function:

$router->get('/movies/{foo}/photos/{bar}', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

Optional Route Subpatterns

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):

$router->get(
	'/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?',
	function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
		if (!$year) { 
			$res->getBody()->write("Blog Overview");
			return $res;
		}
		
		if (!$month) {
			$res->getBody()->write("Blog year overview");
			return $res;
		}
		
		if (!$day) {
			$res->getBody()->write("Blog month overview");
			return $res;
		}
		
		if (!$slug) {
			$res->getBody()->write("Blog day overview");
			return $res;
		}
		
		$res->getBody()->write('Blogpost ' . htmlentities($slug) . ' detail');
		return $res;
	}
);

The code snippet above responds to the URLs /blog, /blog/year, /blog/year/month, /blog/year/month/day, and /blog/year/month/day/slug.

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.

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):

$router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
    // ...
});

Note: It is highly recommended to always define successive optional parameters.

To make things complete use quantifiers to require the correct amount of numbers in the URL:

$router->get('/blog(/\d{4}(/\d{2}(/\d{2zz}(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
    // ...
});

Controllers

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

Anonymous Function Controller

$router->get("/home", function ($req, $res) {
    $res->getBody()->write("Welcome home!");
    return $res;
});

Class Controller

I have a class with a static method, your handler MUST be a static method!

class Home {
	public static function handler($req, $res) {
		$res->getBody()->write("Welcome home!");
		return $res;
	}
}

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.

$router->get("/home", [Home::class, "handler"]);

404 Handler

Defining a 404 handler is required and is similar to creating a route. You can also have different 404 pages for different patterns.

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.

$router->map404("/(.*)", function ($req, $res) {
	$res->getBody()->write("{$req->getRequestTarget()} is not a valid route");
	return $res;
});

500 handler

Defining a 500 handler is recommended and is exactly the same as mapping a 404 handler.

$router->map500("/(.*)", function ($req, $res) {
	$res->getBody()->write("An error has happened internally :(");
	return $res;
});

Note that when in development mode your 500 error handler will be overrode

Middleware

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.

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, you may have a REST API that returns a JSON response. You can have after middleware to make sure the JSON response isn't malformed.

Before Middleware

You can define before middleware similar to a route by providing a method, pattern, and controller. Do note that with beforeMiddleware you're expected to create an object that implements ResponseInterface to pass to the main route.

$router->beforeMiddleware("/admin?(.*)", function (ServerRequest $req, Closure $next) {
	if (!isAdmin()) {
		return new RedirectResponse("/", 403);
	}

	return $next(new Response);
});

In the main route for after middleware

If the router detects after middleware then the third parameter will be a closure with similar functionality to beforeMiddleware

$router->all("/admin/roster/json", function (ServerRequest $req, Response $res, Closure $next) {
	/*
		some code that gets the admin roster and converts to json
	*/

	$res->getBody()->write($jsonString);
	$res->withHeader("content-type", "application/json");

	return $next($res);
});

After Middleware

$router->afterMiddleware("/admin?(.*)", function (ServerRequest $req, ResponseInterface $res) {
	/*
		some code that makes sure the body is a valid json string
	*/

	if (!jsonValid((string)$res->getBody())) {
		return new EmptyResponse(500);
	}

	return $res;
});

Special note about middleware, you can pass variables from beforeMiddleware to the main route or from the main route to afterMiddleware by supplying it as the second argument in the next closure.

Template Engine Integration

You can use 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

use CommandString\Env\Env;

$env = new Env;
$env->twig = new Environment(new \Twig\Loader\FilesystemLoader("/path/to/views"));

// ...

$router->get("/home", function ($req, $res) {
	return new HtmlResponse($env->get("twig")->render("home.html"));\\\
});

Responding to requests

All controllers MUST return an implemantation of \Psr\Http\Message\ResponseInterface or \React\Promise\PromiseInterface. You can use the premade response object passed into the controller or instantiate your own. I recommend taking a look at HttpSoft/Response for prebuilt response types. This is also included with the route as it's used for the dev mode

$response = new HttpSoft\Response\HtmlResponse('<p>HTML</p>');
$response = new HttpSoft\Response\JsonResponse(['key' => 'value']);
$response = new HttpSoft\Response\JsonResponse("{key: 'value'}");
$response = new HttpSoft\Response\TextResponse('Text');
$response = new HttpSoft\Response\XmlResponse('<xmltag>XML</xmltag>');
$response = new HttpSoft\Response\RedirectResponse('https/example.com');
$response = new HttpSoft\Response\EmptyResponse();

Running and Advance Usage

If you want more control over the http server you can use the getHttpServer method to create and return the HttpServer object

$httpServer = $router->getHttpServer();

In addition to being able to retrieve the http server you can also retrieve the socket server with the getSocketServer method

$socketServer = $router->getSocketServer();

Dev Mode

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.

Nodemon

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.

npm install -g nodemon

then in root of your project directory create a new file named nodemon.json and put the following contents into it

{
    "verbose": false,
    "ignore": [
        ".git",
        ".idea"
    ],
    "execMap": {
        "php": "php"
    },
    "restartable": "r",
    "ext": "php,html,json"
}

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!