Hi there, dear reader! I hope you’re having a great day.
Before we dive in, I’d like to share a bit about myself and how I got started in web application development. My journey began with tinkering around with hardware, primarily with Arduinos. I found myself wanting to connect my projects in a more streamlined way, so I started searching for resources. I explored countless books and browsed through many blogs on the vast ocean of the internet. At some point, everything clicked when I discovered the HTTP protocol.
As I experimented with it (alongside HTML, of course), I realized that I needed a method to manipulate the content of web pages to facilitate communication between my various tools. That’s when I discovered PHP.
Today, I am involved in several open-source projects, having led some and contributed to others. I currently maintain two versions of my frameworks: one is called Project-SDF, and the other is proprietary.
In this article, I aim to provide you with an insightful, behind-the-scenes look at how a framework routes requests to their corresponding controllers, handlers, and more.
Basics
The easy way to describe this is to imagine yourself in a restaurant. HTTP protocol works as a middleman (words that we speak to) between you and the waiter. Assume that you want to order food, you select what to eat and what to drink. The waiter takes your order to the kitchen. The chef cooks with sprinkles of love and gives the cooked beauty to the waiter. The waiter brings back your food to you.
So the HTTP works like this;
- You (the client/browser) want to order food (request data)
- The waiter takes your order to the kitchen (the server)
- The kitchen prepares your food and gives it to the waiter
- The waiter brings back your food (the response)

(Assuming you ordered google.com)
Some cool things to know:
- HTTP requests have different types (like GET to fetch data, POST to send data)
- Just like a waiter might ask “How would you like your steak?”, HTTP requests can have specific headers with extra information
- HTTPS is just HTTP with security (like having a private conversation with the waiter)
That’s it! Every time you click a link or submit a form, this little dance happens behind the scenes. Pretty neat, right?
Next Step, Understanding URL’s
So, everyone has heard about this cool kid around, URL here, URL there. But what is a URL exactly?
Think of a URL like your home address but for the internet. Just like your address tells the postal service where to deliver your mail, a URL tells your web browser where to find a specific webpage, image, or file on the vast network we call the internet.
Let’s inspect one to get a better grip on understanding it.
Consider this example:
https://smsk.dev/go/3x2f9?ref=smskdev#comments
Dissecting the URL
https://
: This is the protocol, indicating how we (your browser) communicate with the server. Think of it like choosing between sending a letter or a secure package via postal mail.smsk.dev
: This is the hostname, or, as you may, the address, such as the street name and house number of the site that we want to access./go/3x2f9
: This is a path, similar to finding a specific room in a house. It points to a particular page or resource within the website.?ref=smskdev
: This is the query string, which provides additional information to the web server, like telling the server there is a referrer and it’s referring from “smskdev.”#comments
: This is the fragment identifier, used to jump to a specific section of the webpage, in this case, the “comments” section.
Why are URLs important for routing?
Just like a mail sorter needs to read the address to deliver mail correctly, your PHP router will use the different parts of the URL, mainly the path and sometimes the query string, to decide which part of your code should handle the incoming web request. It’s the crucial link between a user clicking a link and your application knowing what to do!
Now that you have a clearer understanding of what URLs are and their different parts, we can dive deeper into building our PHP router in the next section. Ready to continue?
Building a Simple Router: From URL to Action
Remember our restaurant analogy? The waiter needs to understand your order (the URL) and know which chef to send it to (the corresponding code). Our router will do just that.
Step 1: A Basic Route Handler
Let’s start with a simple example. Imagine we want to display a welcome message when someone visits our homepage (/
) and an “About Us” page when they visit /about
.
$routes = [
'/' => function() { return "Oh, welcome to our restaurant, you can navigate into <a href='about'>to find all about us</a>."; },
'/about' => function() { return "We cook stuff."; }
];
$currentURL = $_SERVER["REQUEST_URI"]; // Get the requested URL
if (array_key_exists($currentURL, $routes)) {
$handler = $routes[$currentURL];
echo $handler(); // we call the handler function as follows.
} else { echo "404, Not Found"; }
In this basic example,
- We store routes as key-value pairs in the
$routes
array. - The key is the URL path, and the value is a function that will be executed when that path is requested.
- We grab the current URL from the
$_SERVER
superglobal. - We check if the URL exists in our
$routes
array. - If found, we execute the associated function (our “route handler”).
- If not, we display a “404, Not Found” message.
This is a very simplified illustration. Real-world routers are much more sophisticated, handling different HTTP methods (GET, POST, PUT, DELETE), dynamic URLs with parameters, and more.
Next Steps: Adding Power to Our Router
We’ve just scratched the surface of routing in PHP. In the upcoming sections, we’ll explore:
- Handling different HTTP methods: What happens when someone submits a form (POST request) versus simply visiting a page (GET request)?
- Dynamic URLs and parameters: How can we create routes like
/products/123
that fetch a specific product based on its ID? - Middleware: How can we perform actions before or after a route handler is executed, like authentication or logging?
Ready to level up our routing game?
Level Up: Handling HTTP Methods
So far, our router treats all requests the same. But in web development, we often need to differentiate between requests based on the HTTP method used (GET, POST, PUT, DELETE, etc.).
- GET: Used to retrieve data (like viewing a webpage).
- POST: Used to send data to the server (like submitting a form).
- PUT: Used to update existing data.
- DELETE: Used to delete data.
Let’s modify our router to handle different HTTP methods:
$routes = [
'GET' => [
'/' => function() { return 'Welcome!'; },
'/about' => function() { return 'About Us'; }
],
'POST' => [
'/contact' => function() {
// Process contact form submission
return 'Message sent!';
}
]
];
$method = $_SERVER['REQUEST_METHOD'];
$currentUrl = $_SERVER['REQUEST_URI'];
if (isset($routes[$method][$currentUrl])) {
$handler = $routes[$method][$currentUrl];
echo $handler();
} else {
echo 'Route not found for this method.';
}
Now:
- Our
$routes
array is structured to store routes based on the HTTP method. - We determine the request method using
$_SERVER['REQUEST_METHOD']
. - We only execute a route handler if it matches both the URL and the HTTP method.
Dynamic URLs: Embracing Flexibility
Hardcoded URLs like /products/123
are limiting. What if we want to display any product based on its ID? Let’s introduce dynamic URLs with parameters:
$routes = [
'GET' => [
'/products/:id' => function($id) {
// Fetch product details from database using $id
return "Product details for ID: $id";
}
]
];
// ... (rest of the code similar to previous example)
// Basic pattern matching (replace with a more robust solution later)
foreach ($routes[$method] as $route => $handler) {
$pattern = str_replace(':id', '(\d+)', $route);
if (preg_match("#^$pattern$#", $currentUrl, $matches)) {
$id = $matches[1]; // Extract the ID
echo $handler($id);
exit;
}
}
// ... (handle 404 if no match found)
Explanation:
- We define a route
/products/:id
, where:id
represents a dynamic parameter. - We use
preg_match
to check if the current URL matches this pattern. - If matched, we extract the ID from the URL and pass it to the handler function.
What’s Next?
We’ve made significant progress! Our router now understands different HTTP methods and can handle dynamic URLs. Here are some features we can add next:
- Robust Pattern Matching: Implement a more reliable and flexible way to match URLs and extract parameters (using regular expressions or dedicated routing libraries).
- Middleware: Add support for middleware to perform actions before/after route execution (e.g., authentication, logging).
- Error Handling: Implement graceful handling of 404 (Not Found) and other error scenarios.
Robust Pattern Matching: Beyond Basic Regex
While our previous preg_match
approach worked for simple dynamic routes, it’s not ideal for more complex scenarios. We need a more robust solution. Let’s introduce named parameters and optional segments using a custom pattern-matching function:
function matchRoute($url, $routeDefinition) {
$pattern = '@^' . preg_replace_callback(
'/\{(\w+)(?:<([^>]+)>)?\}/',
function ($matches) {
$paramName = $matches[1];
$regex = isset($matches[2]) ? $matches[2] : '[^/]+';
return "(?P<$paramName>$regex)";
},
str_replace('/', '\/', $routeDefinition)
) . '$@';
if (preg_match($pattern, $url, $matches)) {
return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
}
return false;
}
$routes = [
'GET' => [
'/users/{id<\d+>}' => function($id) { ... }, // Required ID (digits only)
'/posts/{slug}' => function($slug) { ... }, // Required slug (any characters)
'/articles/{year<\d{4}>}/{month<\d{2}>?}' // Optional month
=> function($year, $month = null) { ... }
],
// ... other methods
];
// ... inside the routing logic ...
foreach ($routes[$method] as $route => $handler) {
$params = matchRoute($currentUrl, $route);
if ($params !== false) {
echo $handler(...array_values($params)); // Pass extracted parameters
exit;
}
}
Now our router supports:
- Named Parameters:
{id}
captures the “id” parameter. - Parameter Regex:
{id<\d+>}
enforces that “id” must be digits only. - Optional Segments:
{month<\d{2}>?}
makes the “month” parameter optional.
Middleware: Adding Layers of Logic
Middleware allows us to execute code before or after a route handler. This is incredibly useful for tasks like authentication, logging, input validation, and more.
$middleware = [
'auth' => function($next) {
// Check if the user is authenticated
if (/* user is authenticated */) {
return $next(); // Proceed to the route handler
} else {
return 'Not authorized!';
}
},
'log' => function($next) {
// Log the request details
error_log("Request: {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}");
return $next(); // Proceed to the next middleware or route handler
}
];
$routes = [
'GET' => [
'/' => [
'middleware' => ['log'], // Apply 'log' middleware
'handler' => function() { ... }
],
'/admin' => [
'middleware' => ['auth', 'log'], // Apply both
'handler' => function() { ... }
]
],
// ... other methods
];
// Middleware execution logic (simplified)
function executeRoute($method, $url) {
// ... (find matching route) ...
if ($route) {
$middlewares = $route['middleware'] ?? [];
$handler = $route['handler'];
// Execute middleware stack (using array_reduce for brevity)
$response = array_reduce(array_reverse($middlewares), function($next, $middlewareKey) use ($middleware) {
return function() use ($middleware, $middlewareKey, $next) {
return $middleware[$middlewareKey]($next);
};
}, $handler)();
echo $response;
} else {
// ... 404 handling
}
}
With this middleware implementation:
- You can define multiple middleware functions.
- You can apply middleware to specific routes or groups of routes.
- Middleware can modify the request/response or short-circuit the process.
This is a basic example, and we can enhance it further with more sophisticated middleware stacks and error handling.
Oh, I promised you a library, didn’t I?
Merging Everything Into a Class
We’ve explored the core concepts of routing, handling different HTTP methods, embracing dynamic URLs, and even adding middleware for enhanced logic. But so far, our code has been a bit scattered.
It’s time to bring it all together into a well-organized and reusable PHP class. This will make our router much easier to work with, maintain, and extend in the future. Here’s a glimpse of what our Router
class might look like:
<?php
class Router
{
protected $routes;
protected $middlewares;
private function matchRoute(string $url, string $route): array|bool
{
$pattern =
"@^" .
preg_replace_callback(
"/\{(\w+)(?:<([^>]+)>)?\}/",
function ($matches) {
$paramName = $matches[1];
$regex = isset($matches[2]) ? $matches[2] : "[^/]+";
return "(?P<$paramName>$regex)";
},
str_replace("/", "\/", $route)
) .
'$@';
if (preg_match($pattern, $url, $matches)) {
return array_filter($matches, "is_string", ARRAY_FILTER_USE_KEY);
}
return false;
}
public function get(string $route, mixed $handler, mixed $middleware = [])
{
$this->routes[] = [
"method" => "GET",
"route" => $route,
"handler" => $handler,
"middleware" => $middleware,
];
}
public function post(string $route, mixed $handler, mixed $middleware = [])
{
$this->routes[] = [
"method" => "POST",
"route" => $route,
"handler" => $handler,
"middleware" => $middleware,
];
}
// ... (Methods for other HTTP verbs: PUT, DELETE, etc.)
public function addMiddleware(string $name, mixed $function)
{
$this->middlewares[$name] = $function;
}
public function dispatch()
{
$url = $_SERVER["REQUEST_URI"];
$method = $_SERVER["REQUEST_METHOD"];
foreach ($this->routes as $route) {
if ($route["method"] !== $method) {
continue;
}
$params = $this->matchRoute($url, $route["route"]);
if ($params !== false) {
foreach ($route["middleware"] as $middleware) {
if (isset($this->middlewares[$middleware])) {
$this->middlewares[$middleware]();
}
}
$route["handler"]($params);
return;
}
}
// Handle 404
echo "404 Not Found";
}
}
Why This Matters
- Clean Code: Our routing logic is now neatly organized within a class, making it more readable and manageable.
- Reusability: We can easily create multiple instances of our
Router
class to handle different parts of our application, or even use it across different projects. - Extensibility: Adding new features or supporting more advanced routing scenarios becomes much simpler when we have a well-defined class structure.
Wrapping Up
Congratulations on making it this far! You’ve gained valuable insights into the inner workings of routing in PHP. You’ve learned how to handle different HTTP methods, create dynamic routes, incorporate middleware, and even package it all up into a neat and reusable class.
This is just the beginning! There’s always more to explore in the world of routing. Consider exploring more advanced concepts like:
- Route Groups: Grouping similar routes for better organization and applying shared middleware.
- Named Routes: Giving your routes memorable names for easier URL generation.
- Reverse Routing: Generating URLs based on route names and parameters.
- Advanced Error Handling: Creating custom error pages and handling HTTP exceptions gracefully.
- Magic Routes: While it might sound unusual, another intriguing concept is Magic Routes. These are routes that are added at the runtime depending on the controller’s name and flags.
Remember, building your router can be an incredibly rewarding experience. It allows you to understand the fundamental principles of web development and tailor your routing logic to the specific needs of your applications.
Happy routing!
Leave a Reply