Creating a Router in PHP

How to create a router in PHP to handle different request types, paths, and request parameters.

620 views

Edited: 2021-07-03 15:55

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 simple route handler might 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.

Here is a visual representation of how a router might be designed:

PHP Router illustration

The user sends a HTTP request to a web application, the router passes the request to a route handler that is configured to handle the specific request type, which finally sends a response back to the user.

Incoming HTTP requests

PHP makes it 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 simple route handler might 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; but an even better option is to use objects, since that will enable suggestions and autocompletion in your editor — to make it more easy to understand, only arrays will be used in this article.

Previously you saw how easy it is to map a static URL like /my/unique-address; now it is time to enable the use of regular expressions.

First keep in mind 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.

This tutorial focuses on functions that are called dynamically.

A simple route definition could look like this:

[
  'string' => '/',
  'methods' => ['GET', 'HEAD'],
  'function' => 'frontpage',
  'get_parms' => ['fbclid']
]

The above gives the router the following instructions:

1. string makes it so that the handler is only triggered by calling the / path. This is the same as example.com/ — the path will always at least contain a forward slash "/", so it will never be empty.

2. methods dictates that only request types of GET and HEAD are accepted, while all other request types are rejected with a 405 response.

3. function should correspond with a "feature_" function used as a handler for the request. You could easily add other handler types, like including a file or instantiating an object.

4. get_parms contains an array of allowed GET parameters. In this case "fbclid", which is used/added by Facebook to links shared on their platform. Same principle applies to post_parms for POST requests.

A complete router-script in PHP

You should now know about the different problems you need to solve to create a working router script, a completed example is included here.

This tutorial only shows how to create a simple non-OOP router, there are multiple benefits to making an OOP-based router — but how to convert this into OOP will be covered in another article.

Full router script:

// 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',
        'get_parms' => ['fbclid']
    ],
    [
        'string' => '/submit-contact-form',
        'methods' => ['POST'],
        'function' => 'submitted_contact_form',
        'post_parms' => ['name', 'email', 'message']
    ],
];

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

    // Verrify that used parameters are allowed by requested resource
    // Note.. A POST request can also contain GET parameters
    //        since they are included in the URL
    //        We therefor verrify both parameter types.
    if (isset($route['get_parms'])) {
        handle_parameters($route['get_parms'], $_GET);
    }
    if (isset($route['post_parms'])) {
        handle_parameters($route['post_parms'], $_POST);
    }

    // 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'])) {
        respond(
            405,
            '<h1>405 Method Not Allowed</h1>',
            ['allow' => implode(', ', $route['methods'])]
        );
    }

    // If everything was ok, try to call the related feature
    if (isset($route['function'])) {

        // Make sure the route handler is callable
        if (!is_callable('feature_' . $route['function'])) {
            $content = '<h1>500 Internal Server Error</h1>';
            $content .= '<p>Specified route-handler does not exist.</p>';
            $content .= '<pre>'.htmlspecialchars($route['function']).'</pre>';
            respond(500, $content);
        }

        // 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
respond(404, '<h1>404 Not Found</h1><p>Page not recognized...</p>');

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

    respond(200, $content);
}

function feature_submitted_contact_form()
{
    $content = '<p>Thank you for contacting us!</p>';
    $content .= '<p>Your message was:</p>';
    $content .= '<pre>' . htmlspecialchars($_POST['message']) . '</pre>';
    respond(200, $content);
}

function respond($code, $html, $headers = [])
{
    $default_headers = ['content-type' => 'text/html; charset=utf-8'];
    $headers = $headers + $default_headers;
    http_response_code($code);
    foreach ($headers as $key => $value) {
        header($key . ': ' . $value);
    }
    echo $html;
    exit();
}

function handle_parameters($allowed_parameters, $post_or_get_parameters)
{

    $invalid_parameters = [];
    foreach ($post_or_get_parameters as $parm_name => $parm_value) {
        if (!in_array($parm_name, $allowed_parameters)) {
            $invalid_parameters[] = $parm_name;
        }
    }


    if ($invalid_parameters !== []) {
        echo '<p><b>Invalid request:</b> parameters not allowed.</p>';
        echo '<pre>';
        foreach ($invalid_parameters as $invalid_key => $invalid_name) {
            echo $invalid_key . ': ' . $invalid_name . "\n";
        }
        echo '</pre>';
        exit();
    }

    return true;
}

Tell us what you think:

  1. How to use the AVIF image format in PHP; A1 or AVIF is a new image format that offers better compression than WebP, JPEG and PNG, and that already works in Google Chrome.
  2. How much faster is C++ than PHP to increment and display a counter in a loop?
  3. Detecting the request method used to fetch a web page with PHP, routing, HTTP responses, and more.
  4. How to create a custom error handler for PHP that handles non-fetal errors.

More in: PHP Tutorials