Basic route management with PHP and Apache httpd

Camilo Herrera
winkhosting
Published in
9 min readFeb 8, 2024

--

Photo by José Martín Ramírez Carrasco on Unsplash

Hello! I hope you’re starting the year with lots of plans and tranquility. Remember that every day we exist is a blessing and we should make the most of it in the best possible way.

Today we are going to study a basic concept for managing a website or web software, which is handling routes. In this article, we will focus on managing routes using PHP, Apache, and mod_rewrite.

Let’s explore how we do it in our company. Generally, our requirements are quite basic, and we don’t need frameworks or libraries with advanced functionalities for this type of tasks.

R.I.P my meme friend, you will always be remembered.

Let’s start as always with our requirements, and then we’ll begin the implementation and explanation of concepts applied to our solution.

Requirements

As in most of our publications, the following elements will be necessary:

  1. A local/remote development environment that includes PHP 8.2, Apache 2.4, and the active mod_rewrite module in Apache.
  2. Coffee, lots of coffee.
  3. Steel will, courage, and eagerness to learn.

The Solution

Everything starts with something small, one short step after another. In this particular case, we’ll begin by creating a directory in our web server’s root directory.

Let’s give it a superhero movie name, “routeman”. If handling routes in web applications were a mutant superpower, this would be part of the X-men team.

Once created, within the folder, create the following files and directories:

  • .htaccess: This file is responsible for configuring redirection and route rewriting on the web server using mod_rewrite. We’ll also add some options in it to control access to PHP files in the directory.
  • index.php: This file will receive all requests made to the URL and will be responsible for defining routes and their configuration.
  • RouteMan.php: The heart of our solution, this class will handle all route management logic and site behavior.
  • /routes: This will be our directory containing PHP files associated with each “file” type route. Later on, we’ll see the two types of routes we can create (“file” and “function”). For the “file” type, our route handler will invoke the corresponding file stored in the /routes folder, as configured in index.php.
  • /routes/default.php: It will show default content when accessing the home URL. This includes a list of available routes and an error (missing) route to demonstrate the behavior when this happens.
  • /routes/about.php: It will display content at the /about route.
  • /routes/testjson.php: It will return content at the /json route with a custom Content-Type “application/json”.

The general structure of our solution regarding directories and files will be as follows:

Files and directories structure

Ignore the .vscode directory; it’s part of the project when you create debugging profiles in VS Code.

Now let’s move on to the Apache HTTP Server configuration in our .htaccess file.

# Restrict direct access to .php files from sources other than localhost.
<Files "*.php">
Require ip 127.0.0.1
</Files>

# Authorize the only entry point for requests coming from external IPs, our index.php file.
<Files "index.php">
Require all granted
</Files>

# Activate the route rewriting engine.
RewriteEngine On

# Uncomment the following lines if you want to enforce access through HTTPS
#RewriteCond %{HTTPS} off
#RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

# Activate the authorization header if you're using PHP in CGI mode.
RewriteCond %{HTTP:Authorization} ^(.)
RewriteRule . - [e=HTTP_AUTHORIZATION:%1]

# Set the mod_rewrite relative path where our files are located.
# If you are going to use a directory at the end of the URL, e.g., http://yourdomain.com/routeman,
# you should set this.
RewriteBase /routeman/

# And now our redirection control. These lines instruct the server
# that any file or directory name in a URL should redirect it
# to the index.php file.
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

With this configuration, we redirect all traffic to index.php, and from there, we can manage the routes using the RouteMan class that we will implement next.

RouteMan.php

<?php

/**
* RouteMan class provides routing functionality for all mankind.
*/
class RouteMan
{
/** @var string The path to the directory of "file" type routes. */
private string $routePath;

/** @var string The default content type. */
private string $defaultContentType;

/** @var array The array of configured routes. */
private array $arrRoutes;

/** @var string The protocol used to access the HTTP/HTTPS route. */
private string $requestURL;

/** @var string The current managed route. */
private string $currentRoute;

/** @var string The rewrite base for routes. */
private string $rewriteBase;

/**
* RouteMan constructor.
*/
public function __construct()
{
// Initialize properties
$this->requestURL = (isset($_SERVER["HTTPS"]) ? "https://" : "http://")
. $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
$this->routePath = 'routes/';
$this->defaultContentType = 'text/html';
$this->currentRoute = '';
$this->rewriteBase = '/routeman';

// Default message when a route is not found.
$this->addRoute('error', [
'function' => function ($route) {
echo "Route " . $route . " not found!";
}
]);
}

/**
* Adds a route to the RouteMan instance.
*
* @param string $url The URL of the route, e.g., "about".
* @param array $routeAttr The file attribute of the route. This parameter
* has the following structure:
*
* array("file" => "name of the php file to use",
* "content" => "text/html")
*
* array("function" => function(){
* echo "hello";
* },
* "content" => "text/html")
*
* @return void
*/
public function addRoute(string $url, array $routeAttr): void
{
$arrURLs = explode(",", $url);

foreach ($arrURLs as $urlItem) {
$this->arrRoutes[$urlItem] = $routeAttr;
}
}

/**
* Manages actions and access to configured routes based on the URL provided.
*
* @return void
*/
public function manageRoutes(): void
{
$this->requestURL = (isset($_SERVER["HTTPS"]) ? "https://" : "http://") . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];

$arrURL = parse_url($this->requestURL);
$arrURLParams = array();

if (isset($arrURL['query'])) {
parse_str($arrURL['query'], $arrURLParams);
}

// Convert parameters sent through JavaScript using .fetch() to a "global" array like $_POST or $_GET
$_RMFETCH = json_decode(file_get_contents("php://input"), true);

if (is_null($_RMFETCH)) {
$_RMFETCH = array();
}

$arrURL["path"] = str_replace($this->rewriteBase, "", $arrURL["path"]);
$this->currentRoute = trim($arrURL['path'], "/");

if (array_key_exists($this->currentRoute, $this->arrRoutes)) {
if (array_key_exists('content', $this->arrRoutes[$this->currentRoute])) {
$this->setHeaderType($this->arrRoutes[$this->currentRoute]['content']);
} else {
$this->setHeaderType();
}

if (isset($this->arrRoutes[$this->currentRoute]['file'])) {
$filePath = $this->routePath . $this->arrRoutes[$this->currentRoute]['file'];

if (file_exists($filePath)) {
require $filePath;
exit;
}
}

if (isset($this->arrRoutes[$this->currentRoute]['function'])) {
$this->arrRoutes[$this->currentRoute]['function']();
exit;
}
} else {
$this->arrRoutes["error"]['function']($this->currentRoute);
exit;
}
}

/**
* Sets the Content-Type to use for each invoked route.
*
* @param string|null $type The content type.
* @return void
*/
public function setHeaderType($type = null): void
{
if (is_null($type)) {
$type = $this->defaultContentType;
}

header("Content-Type: " . $type . "; charset=UTF-8");
}
}

Let’s analyze the source code. In the constructor, we’ll define some default values necessary for the operation of RouteMan. The first of these will be requestURL, where we’ll store the type of protocol used to access our routes (HTTP/HTTPS). This is necessary to create the complete final route.

Next, we’ll set routePath, which contains the directory where files associated with “file” type routes will be searched. Routes of this type will look for the file in the directory /routes, inject it into the script, and then terminate execution.

We also define the default Content-Type to use in our routes in the defaultContentType property, which in this case is “text/html”.

We have an attribute that allows us to save the currently processed route within the instance, which we will call currentRoute, and by default, it will be empty at the start of execution.

Finally, we will have the attribute associated with the base route for rewriting called rewriteBase. This should match the one set in the .htaccess file, for our particular case, “/routeman”.

Now let’s move on to the functions included in the class.

The addRoute() method allows adding the defined routes that the instantiated object will handle. It takes two parameters of type string, $url and $routeAttr, which allow adding routes like this:

<?php
require_once("RouteMan.php");

$objRouteMan = new RouteMan();

// Routes of type "file"
$objRouteMan->addRoute('about', ['file' => 'about.php']);

// Route with custom Content-Type
$objRouteMan->addRoute('json', [
'file' => 'testjson.php',
'content' => 'application/json'
]);

// Route of type "function"
// (anonymous function or you can pass a reference to an existing function)
$objRouteMan->addRoute('testfunct', [
'function' => function () {
echo "<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is a function-generated content</h1>
<p>Great!.</p>

</body>

</html>";
}
]);

As you can see, there are two types of routes available: “file” and “function”. The first one invokes a .php file in the /routes directory and executes the code within it.

The “function” type allows managing routes using anonymous functions or a reference to an existing function in your code.

You can also set the Content-Type per route if you need the server to respond in a particular way to a request.

Finally, we have the function responsible for setting the Content-Type of the server response when making a request to a route, setHeaderType(). This function takes an optional string parameter $type to set the Content-Type to use and modifies the corresponding response header of the server. If you want to set a custom Content-Type per route, you just need to add it in the route configuration by including it in the “content” entry of the $routeAttr array when adding the route.

Next, let’s review the content of the route files. In most cases, it will be a message within a basic HTML structure.

default.php

This file will be displayed by default when accessing http://localhost/routeman. It will show the list of available routes and an example of an unknown route to test the error message if such an event occurs.

<!DOCTYPE html>
<html>

<head>
<title>Default!</title>
</head>

<body>

<h1>Route list:</h1>
<ul>
<li><a href="about">About</a></li>
<li><a href="json">Json</a></li>
<li><a href="anotherroute">Route</a></li>
</ul>
</body>

</html>

about.php

This file will be included and displayed by the site once a visitor enters the URL http://localhost/about.

<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is about</h1>
<p>This is a paragraph.</p>

</body>

</html>

testjson.php

This route file will display a message encoded in JSON format. To serve the content, the Content-Type is changed to “application/json”.

<?php
echo json_encode(array("this is" => "json content"));

Now that we have our route files, our RouteMan class, and .htaccess, let’s move on to the index.php file responsible for making the magic happen.

index.php

<?php

// Include our class file
require_once("RouteMan.php");

// Create an instance of RouteMan
$objRouteMan = new RouteMan();

// Configure our file routes
$objRouteMan->addRoute('', ['file' => 'default.php']);
$objRouteMan->addRoute('about', ['file' => 'about.php']);

// Configure a sample route, changing the Content-Type to JSON
$objRouteMan->addRoute('json', [
'file' => 'testjson.php',
'content' => 'application/json'
]);

// Configure a route using anonymous functions (if that's your thing)
$objRouteMan->addRoute('testfunct', [
'function' => function () {
echo "<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is a function-generated content</h1>
<p>Great!.</p>

</body>

</html>";
}
]);

// Finally, ask our RoutMan to manage our routes.
$objRouteMan->manageRoutes();

Our index.php file includes the RouteMan class file, creates an instance, and configures the routes to be managed. Once configured, the manageRoutes() function is called to execute the instructions based on visitor requests.

And there we have it, our route handler is ready. Elegant, isn’t it?

Now let’s test it. Open your browser and go to http://localhost/routeman (or the URL configured in your development environment, assuming you have one, right?). Then, you can start accessing the routes configured in your index.php, like this:

http://localhost/routeman
http://localhost/routeman/about
http://localhost/routeman/json
http://localhost/routeman/testfunct

You should see results like this:

Default route
About route
Json route
Missing route error message

As you can see, the concept and implementation are quite straightforward. Certain knowledge of web server configuration is required, but overall, managing routes in this way allows for greater control over traffic and the responses you want to give to visitors’ requests.

Remember, at winkhosting.co, we are much more than hosting! Work on your goals for this year, tell your loved ones that you care about them, and we’ll see you in our next post.

--

--