Creating a Router in PHP

How to create a router in PHP that also handles request- methods and parameters.

77 views

Edited: 2021-04-27 17:32

Routing is the art of mapping requested URLs to specific scripts or parts of a web-application; in PHP you can handle HTTP requests in a single location, typically called a router, which will then load the feature that corresponds to the requested path and/or parameters.

Some routers are even designed to handle all the HTTP requests for an application — including those for static files on the file system — this is beneficial if you wish to place the files behind a form-based login, but requires a file handler to enable streaming and send the correct headers.

This tutorial shows how to create your own router, and how to direct all HTTP requests to go through the router script file (E.g. index.php). Note that the parse_url function is used to match the path with mapped URLs. Let's get started!

A bare-minimum router can look like this:

$parsed_url = parse_url($_SERVER['REQUEST_URI']);
if ($parsed_url['path'] === '/my/unique-article-url') {
   
  if ($_SERVER['REQUEST_METHOD'] === 'GET') {
     echo 'Your request was accepted.';
     exit();
  }

}
// Otherwise refuse the request
http_response_code(400); // 400 Bad Request
echo 'Invalid Request';

This is useful for independent scripts, but if you want to make a more complex application, it would be better to create a more centralized router — the following sections shows how to do just that.

Incoming HTTP requests

PHP makes it very easy to work with different parts of a HTTP request; the things you need are available through superglobals such as $_SERVER, $_GET (for GET parameters), and $_POST (for POST parameters).

Note. If you are into object orientated PHP, then it can make sense to create a wrapper object for these variables rather than depending on them directly — read: Avoid directly accessing superglobals in php

There are several different request methods in the HTTP protocol, and while you do not need to understand or know about all of them, your router should be able to reject unsupported methods; those that are not used should be rejected with an appropriate status code, in order to comply with the HTTP protocol.

A client may send a request to your web-application that looks like this:

GET /my/unique-article-url?parm=value HTTP/2
Host: beamtic.com

1. The first thing you should note is the GET part — this is the request method used by the client. In PHP we can obtain the request method through the $_SERVER['REQUEST_METHOD'] superglobal. E.g.:

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
  echo 'Dealing with a GET request.';
  exit();
}

2. The second thing worth noting is the /my/unique-article-url?parm=value part of the request — this part is known as the requested path, and also contains the query string, which consists of one or more parameters (E.g.: ?name=value&other_name=value); you may want to handle GET parameters separately.

You can use the parse_url function to obtain the clean path without the query string — in any case, the parameters will also be available in the $_GET superglobal, so you do not need to parse the query string manually in PHP.

Now, a absolute basic route handler may look like this:

$parsed_url = parse_url($_SERVER['REQUEST_URI']);
if ($parsed_url['path'] === '/my/unique-article-url') {
   
  if ($_SERVER['REQUEST_METHOD'] === 'GET') {
     echo 'Dealing with a GET request.';
     exit();
  }

}
// Otherwise refuse the request
http_response_code(400); // 400 Bad Request
echo 'Invalid Request';

3. The third part is the host header — this header is required in HTTP 1.1 and later. Web servers use the host header to to select the corresponding virtual host (VHOST).

You probably do not need to know about this to create a router, but it can still be useful if you want to reject requests for unhandled hostnames that might make it through a flawed server configuration.

Note. Some servers serve multiple domain names, and depend on the host header to select the correct website. If the header is either missing or contains an unknown hostname, a "default" website may be shown instead.

4. Finally there is the version of the HTTP protocol that the client used to perform the request. This is not something you need to think about, since it is automatically handled when using the http_response_code function to send status codes.

Routing

1. Before you can handle incoming HTTP requests from a single file, you will need to configure your web server, and tell it which file should handle the requests. If you use the Apache HTTP server, then you can either do this from the VHOST configuration files, or you can do it from .htaccess file; to set it up, simply add the following:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ index.php [QSA,L]

Read: Direct all requests to index.php

This will direct all requests to index.php, except for those that maps to physical files located in the web folder.

2. The code from the previous example expects a GET request for the /my/unique-address path. If the request method is of an unsupported type, then you should respond with an appropriate status code; the HTTP protocol dictates that the 405 status (Method Not Allowed) should be used. E.g.:

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
  http_response_code(405);
  header('allow: GET, HEAD');
  echo '<h1>405 Method Not Allowed</h1>';
  exit();
}

Note that an allow header that contains a list of request methods supported by the resource is also included.

Now it is time to make all this more dynamic. A developer should be able to easily add new routes with custom configurations — without having to change the code in the router itself.

Dynamically mapping URLs

In order to allow developers to easily add more routes, as well as allowed- parameters and request methods, you can make the router use Associative Arrays to keep track of routing configurations.

Previously you saw how easy it is to map a static URL like /my/unique-address, but let's try to also allow the use of regular expressions this time.

There are several different ways for a mapped URL (string or pattern) to load a corresponding feature:

  1. Dynamically including a .php file
  2. Dynamically calling a function
  3. Dynamically instantiating a class.

Just keep in mind there are different techniques you can use to call the desired code. In this tutorial we will be calling functions dynamically.

Below is a complete router script in PHP:

// First define some routes for the application
$routes = [
  [
    'pattern' => '/^\/blog\/([a-z0-1_-]+)$/',
    'methods' => ['GET', 'HEAD'],
    'function' => 'blog'
  ],
  [
    'pattern' => '/^\/forum\/([a-z0-1_-]+)$/',
    'methods' => ['GET', 'HEAD'],
    'function' => 'forum'
  ],
  [
    'string' => '/',
    'methods' => ['GET', 'HEAD'],
    'function' => 'frontpage'
  ],
];

// ------------
// ---- The Router
// ------------
$parsed_url = parse_url($_SERVER['REQUEST_URI']);
$requested_path = $parsed_url['path'];
foreach ($routes as $route) {

  // Check if the route is recognized
  if(isset($route['pattern'])) {
    if(!preg_match($route['pattern'], $requested_path, $matches)) {
       continue;
    }
  } else if (isset($route['string'])) {
     if ($route['string'] !== $requested_path) {
       continue;
     }
  } else {
     // If required a parameter was missing (string or pattern)
     throw new Exception("Missing required parameter (string or pattern) in route.");
  }
  
  // Check that the request method is supported
  if(!in_array($_SERVER['REQUEST_METHOD'], $route['methods'])) {
    http_response_code(405);
    header('allow: ' . implode(', ', $route['methods']));
    echo '<h1>405 Method Not Allowed</h1>';
    exit();
  }

  // If everything was ok, try to call the related feature
  if (isset($route['function'])) {
     // If we got any RegEx matches
     if (isset($matches[1])) {
       call_user_func('feature_'.$route['function'], $matches);
     } else {
       call_user_func('feature_'.$route['function']);
     }
  } else {
     throw new Exception("Missing required parameter (function) in route.");
  }
}

// If the route was not recognized by the router
http_response_code(404);
echo '<h1>404 Not Found</h1>';
echo '<p>Page not recognized...</p>';
exit();

// ------------
// ---- Functions
// ------------
function feature_blog(array $matches) {
  http_response_code(200);
  echo '<pre>Sub-path: ' . $matches["1"] . '</pre>';
  echo '<p>Showing the blog</p>';
  exit();
}
function feature_forum(array $matches) {
  http_response_code(200);
  echo '<pre>Sub-path: ' . $matches["1"] . '</pre>';
  echo '<p>Showing the forum</p>';
  exit();
}
function feature_frontpage() {
  http_response_code(200);
  echo '<p>Showing the frontpage</p>';
  exit();
}

Note. In modern PHP applications, you may want to create this as a class that can be instantiated where needed.

Tell us what you think:

  1. How much faster is C++ than PHP to increment and display a counter in a loop?
  2. Detecting the request method used to fetch a web page with PHP, routing, HTTP responses, and more.
  3. How to create a custom error handler for PHP that handles non-fetal errors.
  4. Setting custom HTTP Headers with cURL is useful when changing User Agent or Cookies. Headers can be changed two ways, both using the curl_setopt function.

More in: PHP Tutorials