onnerby/doeroute

A fast and intuitive Router

2.1.2 2021-04-14 09:39 UTC

This package is auto-updated.

Last update: 2024-04-14 16:14:55 UTC


README

A fast and intuitive PHP Router. Don't make things more complicated than they needs to be. But maybe a little more flexible than this awesome simple router

\Doe\Router

The Doe\Router is a (single file) router where you build your routes using subpaths and closures. The advantage is that the closures are only called if the subpath match which makes it SUPER FAST and easy to follow. It also makes it very easy to delegate specific paths to some kind of controller/action-pattern. After I wrote Doe\Router I found FastRoute that is really fast and very similar to this router and probably a bit more flexible when it comes to multiple variables embedded in the path. They are similar in many ways, but the pattern is slightly different with both pros and cons.

Installation

composer require onnerby/doeroute

Basic example

$router = new \Doe\Router(['GET', 'POST']);
$router->path('blog', function($router) {
	// This closure is called if the route starts with /blog

	$router->path('list', 'GET', function ($router) {
		// This is returned when route goes to "/blog/list"
		return "List of all posts";
	});
	$router->path('tags', 'GET', function ($router) {
		// This is returned when route goes to "/blog/tags"
		return "List of all tags";
	});
	$router->pathVariable('/^([0-9]+)$/', function ($router, $postId) {
		// This is returned when route goes to something like "/blog/1234"
		return "Post " . $postId;
	});
});

// Find route and output the results
echo $router->route($_SERVER['REQUEST_METHOD'], $_SERVER['DOCUMENT_URI']);

Controller example

If you are building bigger webapps you may like to delegate routes to some kind of controller. The Doe\Router is not connected to any kind of pattern for this - but it's still super simple to delegate the route.

// Main app
$router = new \Doe\Router(['GET', 'POST']);
// Route everything starting with /project to our \Controller_Project::route
$router->path('project', ['Controller_Project', 'route']);

...

Controller:

class Controller_Project
{
	public static function route($router)
	{
		$controller = new self;
		
		$router->path('list', 'GET', [$controller, 'list'] );

		$router->pathVariable('/^([0-9]+)$/', function ($router, $projectId) use ($controller) {

			// Any generic method needed for everything
			$controller->getProject($projectId);	

			$router->path('overview', 'GET', [$controller, 'overview'] );
			$router->path('save', 'POST', [$controller, 'save'] );

		});

		// Lets also map the "/project" path to the controllers "list" action
		$router->pathEmpty('GET', [$controller, 'list']);

		$router->pathNotFound([$controller, 'error']);

	}

	private function getProject($projectId) { /* Get the project somehow from a database? */ }

	public function list($router)
	{
		// Full path to this route is "/project/list"
		return 'List projects';
	}

	public function overview($router, $projectId)
	{
		// Full path to this route is "/project/1234/overview"
		return 'Project ' . $projectId . ' overview';
	}

	public function save($router, $projectId)
	{
		// Full path to this route is "/project/1234/save"
		return 'Saved project ' . $projectId;
	}

	public function error($router)
	{
		// Anything not found under "/project/xxxxx"
		return 'You look lost. How can I help?';
	}


}

Filters

You may also use filters to execute stuff before the routes.

// In main app
function authorize($router, $verb) {
	// Authorize user somehow
	if (!($user = getUser())) {
		// Returning anything in "before"-filters will interrupt the route.
		return 'You do not have access to this area.';
	}
}

$router = new \Doe\Router(['GET', 'POST']);
$router->filter('authorize', function($router) {
	$router->path('restrictedarea', function ($router) {
		return "Warning: Resticted area. Authorized personnel only.";
	});
});
...

Why another router?

Most routers I've used overcomplicate things. A router is used on the web for parsing the path of the URL to some kind of action. So lets just do that.

A URL is separated by a / and while most routers will define all routes for all paths at once, I decided to parse one segment at a time. I mean - you probably want to do something on each segment anyway in the end. For instance if I build the following routes:

  • /user listing all users
  • /user/123 showing a users profile page
  • /user/123/badges showing a users profile page with the users "badges"
  • /user/123/projects showing a users profile page with the users projects

etc, etc. You will not go directly to define /user/[0-9]+/project but will define the different segments down to the whole final path.

The upside with this is that we don't need to define all routes for each request. Instead we look for the first segments and once that's found, we parse the next segment. There is also the advantage that everything inside /user/[0-9]+ has its own callable where we can check access to the user before sending the route futher down the next segment.