How To Create A Custom PHP Router

Luis Ramirez Jr
Luis Ramirez Jr
hero image

Looking for an easy PHP project to both improve your skills and improve your file navigation?

Well, good news! In this tutorial, I’m going to walk you through step-by-step, how to build a PHP router.

And sure - while it’s true that you don’t necessarily have to build a router to code with PHP, understanding the role of a router and learning to build one is a great beginner project.

With that said, let’s get into it and cover some of the basic questions, and then walk through the code.

What is a PHP Router?

At its core, a router in web development is a piece of software that directs incoming web requests to the appropriate handler.

Think of your web application as a bustling city filled with various destinations (web pages, services, and resources).

A router ensures that when a request arrives, it's guided to the correct destination based on the URL and HTTP method (like GET or POST). This process is essential for a functional and navigable web application.

navigation

It’s not just for navigation though. Routers are everywhere from web development tools to frameworks, and libraries because they solve a fundamental problem: managing web requests efficiently.

Why does this matter? Well for a few reasons.

User experience

If someone gets sent the wrong thing, to the wrong location, or if the load is slow, then it’s going to affect usage, sales, bounce rate, and more.

Resource management

Efficient handling of web requests ensures an optimized utilization of server resources such as CPU, memory, and bandwidth.

This then prevents resource bottlenecks and allows the system to handle more concurrent requests, which of course you need if you want to scale…

Scalability

Proper management of web requests allows your system to handle increased traffic without experiencing performance degradation or downtime.

This scalability is essential for accommodating growth in your user base or sudden spikes in traffic, perhaps from influencer mentions, marketing campaigns, or holiday events.

Cost Savings

By optimizing web request management, you can reduce the infrastructure costs associated with hosting your web application.

Efficient resource utilization means you can achieve better performance with fewer resources, leading to significant cost savings in the long run.

Reliability

Proper management of web requests enhances the reliability and stability of your web application. It minimizes the risk of server crashes or timeouts, ensuring consistent availability for users.

Security

Effective request management can also contribute to security by implementing rate limiting, request validation, and access controls.

These measures help protect your web application from various types of attacks, including DDoS attacks and injection vulnerabilities.

TL;DR

Efficiency matters!

Whether working with a heavyweight PHP framework like Laravel or Symfony or a minimalist setup, routing is a non-negotiable part of the architecture.

Why build my own PHP router?

It’s true that 99% of the time, you'll want to use a router developed by someone else. However, like I said up top, the main reason you'll want to learn to build your own router is for educational purposes.

By building your own router from scratch, you’ll better understand how web applications handle requests. This knowledge then allows you to appreciate the inner workings of frameworks and libraries you'll use in the future, and write better code.

Learning to build your own router empowers you as a developer. It's a stepping stone to mastering web development, giving you the confidence to tackle complex projects and make informed decisions about the tools and frameworks you choose to work with.

It's the difference between knowing how to just drive a car vs. also understanding how the engine works under the hood.

Also? It’ll help you stop from making rookie mistakes…

The simple but flawed approach: File-based Routing

A lot of beginners tackle routing with file-based routing because it’s simple, and because it’s often the built in method.

However, this approach can cause further issues later on, so let me explain.

File-based routing is a straightforward concept in PHP web development, where the structure of the application's files and directories directly corresponds to the URLs.

This method relies on the natural hierarchy of the filesystem to resolve routes, making it intuitive for developers to organize and manage their web applications.

For example

In a file-based routing system, each PHP file in your project can correspond to a specific route. For instance, consider a basic file structure for a web application:

bash/project-root, or /public index.php, or about.php, or contact.php

In this structure, accessing different URLs would lead to the execution of different PHP files:

  • Visiting http://yourdomain.com/ would serve the public/index.php file
  • Visiting http://yourdomain.com/about.php would serve the public/about.php file, while
  • Visiting http://yourdomain.com/contact.php would serve the public/contact.php file

Simple enough, but like I say, it’s not without its issues…

The issues with File-based Routing

There are 4 major issues with this method.

Issue 1: Management gets messy

When using file based routing, managing routes through the filesystem can become cumbersome as your application grows.

A large application might end up with a deep hierarchy of directories and files, making it difficult to navigate and understand the application structure at a glance.

bad structure

Issue 2: Potential vulnerabilities

Exposing your file structure directly through URLs can also lead to security vulnerabilities which hackers can manipulate.

Issue 3: Flexibility

File-based routing can also be inflexible when handling dynamic routes or complex routing patterns.

For example

When creating SEO-friendly URLs or managing routes with variable parameters (e.g. /user/{id}).

Issue 4: Future complexity

Refactoring an application that uses file-based routing can also be quite challenging.

For example

Changing a file's location or renaming a directory might necessitate updating URLs across the entire application, leading to potential errors and broken links, and a whole heap of effort.

TL;DR

File-based routing offers a simple and intuitive way to handle routing in PHP applications, particularly for small projects or prototypes. However, its limitations become apparent as applications grow in complexity and scale.

For the sake of this exercise, let’s assume that we’re not going to be using a PHP framework that can deal with these issues, and go ahead and build our own router 😀.

Building our router

First, we'll create a file called Router.php with the following code:

declare(strict_types=1);
class Router {
} 

While not strictly necessary, let’s go ahead and enable strict typing. It's good practice, and will help prevent us from passing invalid values, while also providing a simple way to document our code.

Then, after enabling this feature, we have a class called Router.

This singular class will be responsible for a few things:

  1. Storing a list of routes
  2. Registering and formatting new routes
  3. Calling a function based on a route

Let's go through each of these steps.

Step 1. Storing Routes

Step one is arguably going to be the easiest step. We'll store our routes in an array, like so:

private array $routes = [];

Why do this?

Well, most applications will have dozens of routes, so using an array is your best bet for storing all of them. However, it’s not perfect…

Step 2. Registering a Route

Using an array to store routes works, but we shouldn't directly push routes into it.

Instead, it's considered good practice to have a method for registering new routes. This way, we can make sure all data is correctly formatted and inserted without typos.

Let's define a method called add().

public function add(string $method, string $path, array $controller) {
}

For this, there will be three pieces of data we'll need, which are:

  • The HTTP method
  • The URL path
  • And a controller

Within this method, we'll normalize the path. (Normalizing is another way of saying formatting a value to be consistent).

For example

The following could be passed in as valid paths:

  • /about/john
  • about/john
  • /about/john/

They're all the same path but PHP considers them to be different values, so for consistency, we'll format the values to always start and end with a / character.

This means that the normalized path would be /about/john/ and our code would look something like this:

public function add(string $method, string $path, array $controller) { $path = $this->normalizePath($path); }
private function normalizePath(string $path): string { $path = trim($path, '/'); $path = "/{$path}/"; $path = preg_replace('#[/]{2,}#', '/', $path);
return $path; } 

Note that we're outsourcing the logic for normalizing the path in a separate method called normalizePath().

In this method, we're removing excessive / characters, adding the / character at the end and start of the path, and lastly, using a regular expression to remove consecutive / characters.

After normalizing the path, you can push the path into the router like so.

public function add(string $method, string $path, array $controller) { $path = $this->normalizePath($path);
$this->routes[] = [ 'path' => $path, 'method' => strtoupper($method), 'controller' => $controller, 'middlewares' => [] ]; } 

Step 3. Dispatching a Route

The last step in this process is to dispatch the route, which describes the process of selecting a route based on the path.

For this part of the process, we'll have a method called dispatch() with one argument to accept the path, like so:

public function dispatch(string $path) { $path = $this->normalizePath($path); }

However, before doing anything else, the $path argument should be normalized since the routes registered with the router are also normalized.

Another feature of a router is being able to register routes for specific HTTP methods. To dispatch a route with an HTTP method, we'll need to grab it via the $_SUPER superglobal variable.

$method = strtoupper($_SERVER['REQUEST_METHOD']);

Here’s what's happening:

The $_SERVER['REQUEST_METHOD'] stores the HTTP method sent with a request.

And just in case, we're also passing on the value to the strtoupper() function to make sure the HTTP method is consistently formatted, as different browsers may send it in different formatting.

After grabbing all this information, we can loop then through the routes with a foreach loop, like so:

foreach ($this->routes as $route) { if ( !preg_match("#^{$route['path']}$#", $path) || $route['method'] !== $method ) { continue; } }

In the loop, we're performing a conditional statement that uses a regular expression.

The expression merely checks if the path in the current iteration matches the path being visited. If there isn't a match, the next path is checked.

Then, another condition is added with the || operator. For the second condition, the HTTP methods are compared.

So, both the path and HTTP method must match for the route to be considered valid, and if we find a match, we'll grab the controller.

What are controllers?

Controllers are classes responsible for rendering a response, and a response can be anything from an HTML document to an image.

When registering a route, routes must contain the class name and the function inside the class to call when there's a match.

For example

Here's how we would grab the controller.

[$class, $function] = $route['controller'];

We're using a feature of array called destructuring. The value will be an array where the first item is the class name and the second item is the function name.

In the example, we're extracting these values into separate variables for extra clarity.

Instantiating a Class

Once we have the class and function, we can instantiate the class.

PHP supports creating instances of classes with strings. Therefore, the following is allowed:

$controllerInstance = new $class;

Please keep in mind that the class name must be valid, and any typos would result in an error.

After instantiating the class, we can invoke the function defined in the class.

$controllerInstance->{$function}();

Similar to creating instances, we're allowed to call functions with strings.

After the -> operator, we must wrap the function name with a pair of {}. Within these characters, we can then pass in the function name as a string.

And just like that, our router is finished!

Here's what the final class looks like:

declare(strict_types=1);
class Router { private array $routes = [];
public function add(string $method, string $path, array $controller) { $path = $this->normalizePath($path);
$this->routes[] = [
  'path' => $path,
  'method' => strtoupper($method),
  'controller' => $controller,
  'middlewares' => []
];

}
private function normalizePath(string $path): string { $path = trim($path, '/'); $path = "/{$path}/"; $path = preg_replace('#[/]{2,}#', '/', $path);
return $path;

}
public function dispatch(string $path) { $path = $this->normalizePath($path); $method = strtoupper($SERVER['REQUESTMETHOD']);
foreach ($this->routes as $route) {
  if (
    !preg_match("#^{$route['path']}$#", $path) ||
    $route['method'] !== $method
  ) {
    continue;
  }

  [$class, $function] = $route['controller'];

  $controllerInstance = new $class;

  $controllerInstance->{$function}();
}

} } 

Step 4. Configuring the Server

So now that we have it created, the question becomes “How would we use this router to improve our URL structure navigation?

Well, first of all, you have to make sure to capture all requests, as by default, most servers are configured for file-based routing. (And we know that can have some issues).

This means that if you want to handle all requests with PHP, you’ll have to update Apache to grab all requests.

This is done by configuring Apache to redirect all requests to a single index.php file for handling routing involving using a .htaccess file. This file then allows you to define rules for the Apache web server on how to handle requests without modifying the main server configuration.

Let’s walk through it…

Enable mod_rewrite

First, ensure that the mod_rewrite module is enabled in your Apache configuration. This module is required for URL rewriting. (Your hosting provider should provide this as an option).

Alternatively, if you're handy with the command line, you can enable it by running sudo a2enmod rewrite and restarting the Apache server.

Create/Edit .htaccess File

Next, navigate to the root directory of your web application, where your index.php file is located.

If a .htaccess file doesn't already exist in this directory, create one. Then, edit the .htaccess file to include the following rules:

RewriteEngine On # Turn on the rewriting engine RewriteBase / # Optional: Specify the base URL for per-directory rewrites

Don't rewrite files or directories that exist

Then add in RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d

Rewrite all other URLs to index.php

And finally, add RewriteRule ^ index.php [QSA,L]

Here's what each line does:

  • RewriteEngine On: Enables the runtime rewriting engine
  • RewriteBase /: This line is optional. It sets the base URL for rewrite rules. Adjust it according to your application's directory structure
  • RewriteCond %{REQUEST_FILENAME} !-f: Specifies a condition that the requested filename is not an existing file
  • RewriteCond %{REQUEST_FILENAME} !-d: Specifies a condition that the requested filename is not an existing directory
  • RewriteRule ^ index.php [QSA,L]: Redirects all requests (that do not match existing files or directories) to index.php. The QSA (Query String Append) flag ensures that query strings are forwarded to index.php. The L (Last) flag indicates that this is the last rule to be applied if this rule matches

Step 5. Using the Router

After configuring the server like so, we can now use our router!

First, let's create a controller that can render two pages.

class SiteController { public function home() { echo "Home page!"; }
public function about() { echo "About page!"; } } 

Next, we can create an instance of the router and register these routes.

$router = new Router();
$router->add('GET', '/', [SiteController::class, 'home']); $router->add('GET', '/about', [SiteController::class, 'about']); 

During registration, the last argument for the add() method is an array with the class and function name.

For the class name, you could use a plain old string, but PHP defines a constant on all classes called class, which you can then access to grab the full class name. (It's safer to use and avoids the scenario of having typos).

After registering our routes, the last thing we have to do is dispatch the route.

$path = parseurl($SERVER['REQUESTURI'], PHPURL_PATH);
$router->dispatch($path); 

In the example above, we're grabbing the path with the $_SERVER['REQUEST_URI'] superglobal variable. However, this variable contains the full HTTP URL, and we're only interested in the path, so we're using the parse_url() function to extract it.

Lastly, we just call the dispatch() function with the path. That should render either the home page or about page based on what path you visit on your site.

What else can we do with our router?

There are so many other features we could add, such as:

  • Dynamic parameters
  • Middleware
  • Error handling
  • Grouping
  • Etc

These are additional features you would find in most modern routers.

If you're interested in building a more sophisticated router, check out my upcoming PHP course... it's launching soon 😬.

I’ll be covering this topic and more, as well as go over how to add support for dynamic routes and integrate dependency injection into the router.

Don’t miss out!

Simply becoming an email subscriber via the link below and you'll get an email when my PHP course is live, as well as any information on any other ZTM course launches, updates, blog posts, news and tutorials.

For example

We recently ran a study of the top 12 languages to learn this year, and PHP was a surprisingly strong choice, thanks to current job demand, salary, and learning difficulty (beating out some ‘popular’ languages!).

For now though, build your new PHP router and test it out.

Learning by building is far better than just reading or watching a tutorial. Even copying the code and steps here will help you grasp the principles easier, and remember them for longer, so get to work and go build 😜!

More from Zero To Mastery

Best Programming Languages To Learn In 2025 preview
Best Programming Languages To Learn In 2025

Want to know the best programming languages to learn in 2025? See the top 10 picks for high-paying jobs, future-proof skills, and unstoppable growth.

Best Places To Learn To Code In 2025 preview
Popular
Best Places To Learn To Code In 2025

With 100's of sites to learn to code from online, which platform is the best for you? We got you. Here are the pros & cons for 14 of the best sites.

Top 5 In-Demand Tech Jobs For 2024 (+ How To Land A Job In Each!) preview
Top 5 In-Demand Tech Jobs For 2024 (+ How To Land A Job In Each!)

Want to get hired in a tech job in 2024? Pick one of these 5 if you want: 1) High salary 2) Jobs available now 3) Can learn the skills as a complete beginner.