Building a Basic Router with PHP: A step-by-step guide for web developers

Onur Yüksel
Jotform Tech
Published in
6 min readMar 26, 2023

When building a website or web application, having a router is essential for directing user requests to the appropriate pages or endpoints. While many router libraries and frameworks are available for PHP, creating your own basic router can be a great way to understand how routing works. At least for me, replicating or improving something is the best way to learn it.

In this article, we’ll walk through building a basic router with PHP. We’ll start by discussing what a router is and why it’s important. Then, we’ll dive into the code and explore how to set up routes, match URLs to those routes, and handle requests appropriately. By the end of this article, you’ll have a solid understanding of how routers work and how to build your own basic router in PHP.

Let’s get started!

What is a router?

Routers are essential components of many web frameworks and applications, providing a flexible and powerful way to handle requests and build complex web applications. By using a router, you can create a clean and organized application architecture that is easy to maintain and scale over time. In a nutshell, a router is responsible for mapping requests to a function in your web application.

How does a router work?

Generally, the router is the first component of the application to receive requests from users. When the application starts up, the router registers all the available endpoints (or routes) to an array.
When a user sends a request to the server, the router first analyzes the URL provided in the request. It then compares this URL to the array of registered endpoints to determine whether there is a match. If the router finds a match, it knows which function or controller to execute in response to the request.
For instance, if a user sends a request to the URL path “/blog,” the router will look for a matching registered route that specifies the action or controller method to handle the request. If a match is found, the router executes the corresponding function or method and returns the results as a response to the user.
Routers can also handle more complex requests that contain additional parameters or values. In these cases, the router is responsible for parsing the URL and passing any relevant parameters or data to the corresponding function or controller.

A simple routing process

Creating our own router

Before starting to code, it’s important to define what our router will be responsible for. In this case, our simple router will be responsible for registering routes, matching a given URL with predefined routes, handling parameters, executing the corresponding function, and returning the response. If no matching route is found, the router will throw an exception indicating that the requested route was not found.
First, we can start with a router class that stores our routes in an array.

<?php

class Router {
protected $routes = []; // stores routes
}

To register our routes, we need a publicly accessible method. A route will consist of three parts: METHOD, URL, and TARGET FUNCTION. Therefore, we will create a method that takes three arguments and registers our route.

class Router {
protected $routes = []; // stores routes

/*
* add routes to the $routes
*/
public function addRoute(string $method, string $url, closure $target) {
$this->routes[$method][$url] = $target;
}
}

Now, we will be able to register our routes via the addRoute() method. As you can see from the function, the addRoute() method adds a route as $routes[$method][$url] = $target.
Now our router needs to match requests with the given routes.

<?php

class Router {
protected $routes = []; // stores routes

public function addRoute(string $method, string $url, closure $target) {
$this->routes[$method][$url] = $target;
}

public function matchRoute() {
$method = $_SERVER['REQUEST_METHOD'];
$url = $_SERVER['REQUEST_URI'];
if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $routeUrl => $target) {
// Simple string comparison to see if the route URL matches the requested URL
if ($routeUrl === $url) {
call_user_func($target);
}
}
}
throw new Exception('Route not found');
}
}

Note that the implementation for addRoute() does not include any pattern matching at this time, as this will depend on the specific needs of the project. The implementation for matchRoute() simply checks if the requested method and URL match any of the stored routes and returns the corresponding target if a match is found.
Now we have a simple routing logic with limited capabilities. To test the router, we need to rewrite Apache’s redirection rule. To do this, create a .htaccess file in your application’s directory.

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]

This way, we have redirected all requests to our index.php file. Now we can test our router by creating an index.php file.

<?php

include_once "router.php";
$router = new Router();

$router->addRoute('GET', '/blogs', function () {
echo "My route is working!";
exit;
});

$router->matchRoute();

Now, when we go to http://localhost:8000/blogs, we will be able to see the message “My route is working!” If we go to an unregistered route, we will see the message “Route not found.”
Our simple router works, but has limited capabilities. In real life, a route is usually more complicated than “/blogs.” Routes can include various parameters like id and slugs. To achieve this, we must refactor our matchRoute() method.

public function matchRoute() {
$method = $_SERVER['REQUEST_METHOD'];
$url = $_SERVER['REQUEST_URI'];
if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $routeUrl => $target) {
// Use named subpatterns in the regular expression pattern to capture each parameter value separately
$pattern = preg_replace('/\/:([^\/]+)/', '/(?P<$1>[^/]+)', $routeUrl);
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// Pass the captured parameter values as named arguments to the target function
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); // Only keep named subpattern matches
call_user_func_array($target, $params);
return;
}
}
}
throw new Exception('Route not found');
}

So, we have been able to handle URL parameters. The key part is:

$pattern = preg_replace('//:([^/]+)/', '/(?P<$1>[^/]+)', $routeUrl);

The regular expression pattern /\/:([^\/]+)/ matches a substring in the route URL that starts with a forward slash (/) followed by a colon (:), and captures the characters after the colon until the next forward slash (/).

The preg_replace() function then replaces all occurrences of this pattern in the route URL with a new string, /(?P<$1>[^/]+).

(?P<$1>[^/]+) captures one or more characters that are not a forward slash, as before, but uses a named subpattern to give the captured substring a name. The <$1> syntax specifies the name of the subpattern, which is the same as the name of the capturing group in the original pattern.

Now, we can test our router with parameters.

$router->addRoute('GET', '/blogs/:blogID', function ($blogID) {
echo "My route is working with blogID => $blogID !";
exit;
});

Note that we have used named subpatterns. This means you need to write the correct parameter name to retrieve the URL. For example, if you use the parameter name :blogID, you cannot use $id as a parameter. You need to pass $blogID as it is typed in the URL.

Conclusion

In this post, we have learned what a router is, what it does, and how it works. We have successfully implemented a core router that can handle named parameters. Note that this router example is very simple and serves as a good blueprint to start with. We can improve the logic and wrap this concept around design patterns such as the builder, decorator, and responsibility chain.

Sign up to discover human stories that deepen your understanding of the world.

Published in Jotform Tech

Welcome to Jotform official tech blog. Read about software engineering and how Jotform engineers build the easiest form builder.

Written by Onur Yüksel

Jr Backend Developer @Jotform. Passionate about building scalable apps. Sharing insights on tech, best practices, and experiences. Let's connect!

No responses yet

Write a response