Simple routing system for a PHP MVC application
Micael Vinhas
Posted on August 16, 2022
You don't want to use a framework and yet you want to get rid of messy URLs and complex routing systems? Maybe I have the solution for you.
Any PHP framework can handle extremely well the routing/dispatching system, but not everyone is interested in a framework to do their job. You came here because you want to do your own routing system, right? Come along, I'll show you my approach to this issue and who knows it will help you to get an ever effective way to do a proper routing system.
Understand what you need to grab to build your function call
There is a superglobal that will help you in this challenge: $_SERVER
. Just like the PHP documentation says:
$
_SERVER
is an array containing information such as headers, paths, and script locations. The entries in this array are created by the web server. There is no guarantee that every web server will provide any of these; servers may omit some, or provide others not listed here.
PHP warns you about the potential absence of information, but the main one we will use, PATH_INFO
, is very likely to be provided. Even if PATH_INFO
is not there (for example, if no actual path is provided), we can create a default value, say '/'.
Without any further introduction, let's write some code!
**Warning: at the time of this writing I'm using PHP 8.1. Please note that the following code may or may not work in your environment, depending not only on the PHP version but also on your web server.
1. Get the URI
The first step is to create a function that simply grabs the URI and breaks it into many parts. These parts will represent the controller, method and method parameters (args).
private static function getURI() : array
{
$path_info = $_SERVER['PATH_INFO'] ?? '/';
return explode('/', $path_info);
}
So, for a URL like this:
http://www.example.com/posts/view/3
You will get:
$uri[0] = 'posts';
$uri[1] = 'view';
$uri[2] = '3';
Now you can already imagine a posts controller, with a method called view that receives as an argument post_id.
2. Process the getURI information
Let's build an object that returns the controller, method and args (if any):
private static function processURI() : array
{
$controllerPart = self::getURI()[0] ?? '';
$methodPart = self::getURI()[1] ?? '';
$numParts = count(self::getURI());
$argsPart = [];
for ($i = 2; $i < $numParts; $i++) {
$argsPart[] = self::getURI()[$i] ?? '';
}
//Let's create some defaults if the parts are not set
$controller = !empty($controllerPart) ?
'\Controllers\\'.$controllerPart.'Controller' :
'\Controllers\HomeController';
$method = !empty($methodPart) ?
$methodPart :
'index';
$args = !empty($argsPart) ?
$argsPart :
[];
return [
'controller' => $controller,
'method' => $method,
'args' => $args
];
}
Bear in mind that you can simplify your use case and write everything on the same function since it's unlikely that we need to call getURI one more time. I'm separating both for Single-responsibility principle (SRP) purposes.
The controller used here is just an example, with a namespace Controllers. The common convention is Controllers\SomethingController, and I like it.
3. Create a function that wraps up and calls the appropriate controller, method and its arguments
This is the funniest part because you already made the dirty job of deconstructing the URI:
public static function contentToRender() : void
{
$uri = self::processURI();
if (class_exists($uri['controller'])) {
$controller = $uri['controller'];
$method = $uri['method'];
$args = $uri['args'];
//Now, the magic
$args ? $controller::{$method}(...$args) :
$controller::{$method}();
}
}
(Those three dots before the $args array are called array unpacking. Basically, PHP will extract the array values and instantiate them as separate variables.)
But wait! What happened here?
Let's pick up again our previous example, but with an extra argument:
http://www.example.com/posts/view/3/excerpt
In this case, we have:
Controller: \Controllers\PostsController
Method: view()
Args: ('3', 'excerpt')
So, our call will be: \Controllers\PostsController::view('3', 'excerpt'). Essentially, the excerpt of the post with an id equal to '3'.
Very simple, yet effective. If no method is passed, we will assume index as our default method. But beware, the application will throw an error if you don't have any default method called index and you don't explicitly it either.
How can we call this?
Route::contentToRender();
This is assuming that we called the class Route. And please note that we are also assuming that our controller methods are static. If they are not static you have to make a little change to contentToRender() function.
Instead of:
$args ? $controller::{$method}(...$args) :
$controller::{$method}();
You have to write:
$args ? (new $controller)->{$method}(...$args) :
(new $controller)->{$method}();
I use static methods a lot and I know they are harder to test. Karma will kill me sooner or later for doing that.
Wrapping up, here is our class Route.php:
<?php
class Route
{
public static function contentToRender() : void
{
$uri = self::processURI();
if (class_exists($uri['controller'])) {
$controller = $uri['controller'];
$method = $uri['method'];
$args = $uri['args'];
//Now, the magic
$args ? $controller::{$method}(...$args) :
$controller::{$method}();
}
}
private static function getURI() : array
{
$path_info = $_SERVER['PATH_INFO'] ?? '/';
return explode('/', $path_info);
}
private static function processURI() : array
{
$controllerPart = self::getURI()[0] ?? '';
$methodPart = self::getURI()[1] ?? '';
$numParts = count(self::getURI());
$argsPart = [];
for ($i = 2; $i < $numParts; $i++) {
$argsPart[] = self::getURI()[$i] ?? '';
}
//Let's create some defaults if the parts are not set
$controller = !empty($controllerPart) ?
'\Controllers\\'.$controllerPart.'Controller' :
'\Controllers\HomeController';
$method = !empty($methodPart) ?
$methodPart :
'index';
$args = !empty($argsPart) ?
$argsPart :
[];
return [
'controller' => $controller,
'method' => $method,
'args' => $args
];
}
}
Do you have another way to make this?
I'm pretty sure you do! I'm always into cleaner and shorter code, so if you have any proposals to improve this approach, please let us know in the comments. That way, we, PHP fanboys, can get the PHP awesomeness bar even higher!
Have a great day!
Posted on August 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.