Vanilla Blog — Part 1 | Basic Router

Mehdi JaiMehdi Jai
5 min read

Other Parts:


By default, PHP routing follows file-based routing, where URLs are structured as /page-1.php. However, the custom router we will develop will enable a cleaner URL structure such as /page-1/subpage. This system will also allow us to utilize parameterized URLs like: /post/slug, resulting in a more professional and user-friendly appearance.

How does it work

The .htaccess file we created earlier will assist us in redirecting all routes to the index.php file. Afterward, we can extract the URI from the $_SERVER superglobal variable. For instance, when we visit: /some-page, we can retrieve the route by accessing $_SERVER['REQUEST_URI']. This allows us to display the appropriate content associated with that particular URI.

Create the basic Router

// public/index.php

if(preg_replace("/(^\/)|(\/$)/", "", $_SERVER['REQUEST_URI']) == 'about') {
    view('about');
}

This code will check what the URI is about. If so, it will require the about.view.php file (which contains the About page content).

Build the Advanced Router MVP

For advanced and complex usage, relying solely on basic routing might not be efficient. In such cases, we can create a Router class to handle incoming URIs more effectively. I have built a router that resembles Laravel's routing approach, where we can define routes and match them against incoming requests. If a match is found, we can then require the corresponding controller (although, for now, we can directly use views at this stage).

This approach will provide more flexibility and scalability for handling various routes and their corresponding actions or controllers.

How does it work

To begin with, we can create an array to store our routes. At the end of the file, we will execute a route method to check if the URI matches any defined routes, and if a match is found, we can render the associated view. This approach allows for a clear separation of routes and view rendering.

Code time

Note: The router class will be a singleton

// app/Core/Router.php
<?php

class Router 
{
    // Handle singlton
    private static $instance = null;
    private array $routes = [];

    // to store parameters and queries and pass them to view file
    private array $data = []; 

    public static function getInstance()
    {
        if (self::$instance == null) {
            self::$instance = new Router();
        }
        return self::$instance;
    }

   // Add route to routes array
    private function add(string $uri, string $view)
    {
        if ($this->routes == null) {
            $this->routes = new RouteCollection([]);
        }

        $this->routes[] = compact("uri", "view");

        /**
        /* compact helper function returns an array with associated keys
        /* from variable name. The result will be:
        /* [
        /*      "uri" => $uri,
        /*      "view" => $view
        /* ]
        */

        return $this; // for methodes chaining.
    }

    public function route()
    {
        foreach ($this->routes as $route) {
            if ($this->matchRoute($route['uri'])) {
                view($route['view']);
                exit();
            }
        }

        abort(404); // If nothing matches, Abort with not found exception
    }

   private function matchRoute(string $routeUri): bool
    {
            // Get only route name and exclude queries.
            $server_uri = preg_replace("/(^\/)|(\/$)/", "", parse_url($_SERVER['REQUEST_URI'])['path']);

        if (!empty($server_uri)) {
            $routeUri = preg_replace("/(^\/)|(\/$)/", "", $routeUri);
            $reqUri =  preg_replace("/(^\/)|(\/$)/", "", $server_uri);
        } else {
            $reqUri = "/";
        }

        return $reqUri == $routeUri;
    }
}

Let's update index.php

// public/index.php

<?php

// ---- BEGIN:CONSTANTS ----

const BASE_PATH = __DIR__ . '/../';
const PUBLIC_PATH = __DIR__;

const HOMEPAGE = "/";
const LOGIN_PAGE = "/auth/login";
const REGISTER_PAGE = "/auth/register";
const PROFILE_PAGE = "/auth/profile";

// ---- END:CONSTANTS ----

require BASE_PATH . "app/functions/utils.php";

require base_path("app/Core/Router.php");
require base_path("routes/web.php"); // Where we handle the routes

Let's add some routes to web.php

// routes/web.php

<?php

$router = Router::getInstance(); // Singleton instance

$router->add("/", "home"); // URI, View name
$router->add("/about", "about");
$router->add("/contact", "contact");

$router->route(); // Find match, if no match found, abort with 404

Let's add the views. We will need four views:

  • Home: home.view.php

  • About: about.view.php

  • Contact: contact.view.php

  • 404: 404.view.php (We will put it inside Errors directory later)

// views/404.view
<?php component("head") ?>

<main class="grid min-h-full place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
    <div class="text-center">
        <p class="text-base font-semibold text-indigo-600">404</p>
        <h1 class="mt-4 text-3xl font-bold tracking-tight text-gray-900 sm:text-5xl">Page not found</h1>
        <p class="mt-6 text-base leading-7 text-gray-600">Sorry, we couldnt find the page youre looking for.</p>
        <div class="mt-10 flex items-center justify-center gap-x-6">
            <a href="/" class="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Go back home</a>
            <a href="/contact" class="text-sm font-semibold text-gray-900">Contact support <span aria-hidden="true">&rarr;</span></a>
        </div>
    </div>
</main>

<?php component("footer") ?>
// views/about.view
<?php component("head") ?>

<main>
    About
</main>

<?php component("footer") ?>
// views/contact.view
<?php component("head") ?>

<main>
    Contact
</main>

<?php component("footer") ?>
// views/home.view
<?php component("head") ?>

<main>
    Home
</main>

<?php component("footer") ?>

Conclusion.

In this section, we learned how to build a basic routing system. We previously discussed setting up the environment to handle this routing, which you can review in the First Step section. However, this basic router is limited and does not account for handling different HTTP methods. To overcome this limitation, we will need to introduce controllers, which will handle the logic and determine the appropriate view for each route and method.

See you in the next one!

1
Subscribe to my newsletter

Read articles from Mehdi Jai directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mehdi Jai
Mehdi Jai

Result-oriented Lead Full-Stack Web Developer & UI/UX designer with 5+ years of experience in designing, developing and deploying web applications.