Simple routing system for a PHP MVC application
Tags: php, routing, programming, webdev
Published: 2022-08-16 13:48:00

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! ![⌨️](https://s.w.org/images/core/emoji/14.0.0/svg/2328.svg)

***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](https://en.wikipedia.org/wiki/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](https://www.php.net/manual/en/migration56.new-features.php)**. 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!