DEV Community

Micael Vinhas
Micael Vinhas

Posted on

Simple routing system for a PHP MVC application

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.

https://www.php.net/manual/en/reserved.variables.server.php

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);
}
Enter fullscreen mode Exit fullscreen mode

So, for a URL like this:

http://www.example.com/posts/view/3
Enter fullscreen mode Exit fullscreen mode

You will get:

$uri[0] = 'posts';
$uri[1] = 'view';
$uri[2] = '3';
Enter fullscreen mode Exit fullscreen mode

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
    ];
}
Enter fullscreen mode Exit fullscreen mode

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}();
    }
}
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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}();
Enter fullscreen mode Exit fullscreen mode

You have to write:

$args ? (new $controller)->{$method}(...$args) :
        (new $controller)->{$method}();
Enter fullscreen mode Exit fullscreen mode

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
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (7)

Collapse
 
gbhorwood profile image
grant horwood

i took a somewhat different approach, but would be interested in your insights.
github.com/gbhorwood/tangelo/blob/...

Collapse
 
mvinhas profile image
Micael Vinhas

Will take a look, thank you.

Collapse
 
nicolasdanelon profile image
Nicolás Danelón

Hahaha it's super clean and beautiful. I love that router 👌🏻

Collapse
 
mvinhas profile image
Micael Vinhas

Thank you so much Nicolas :)

Collapse
 
ribafs profile image
Ribamar FS

Please, how to use this classe?
One example.

Collapse
 
mvinhas profile image
Micael Vinhas

Hello,

You can include this class on index, and then call it like (new Route)->run();

On the "run" function you have to call the parts of your webpage that you want to display. For example:
public function run(): void
{
(new Db())->initialize();
(new SiteController())->head();
(new SiteController())->header();
(new Route())->contentToRender();
(new SiteController())->footer();
}

And "ContentToRender" will render whatever is in your URL

Collapse
 
gopego profile image
Fahmi

Thank you for the tutorial, the hard part is to integrate this to my project, hope i won't mess it up hahaha