Simple: Custom Web Router

by | Nov 26, 2021 | Uncategorized | 0 comments

Sometimes we get so experienced in our field we need to revisit fundamentals. Today we will look at writing a simple web router in PHP.

Love of Frameworks

I’ve been developing web applications for a few years now using frameworks such as WordPress, Symphony, and Laravel. One thing that they all share in common is providing us base services such as routing, authentication, and database connection management so we can focus on writing new code instead of re-writing code that all project share in common. One benefit of all these free services is they are developed and maintained by a team of excellent developers who take the time to focus on these underlying systems to ensure they are performant and secure. It is not to say other developers such as myself can’t make these services, it is simply freeing us up from the chore of writing code that really doesn’t make a positive impact in a projects purpose.

Downside of Frameworks

Whether you are a new developer starting with frameworks out of the gate or a senior knowing all the frameworks special functions it’s probably safe to say we get comfortable with all the helpful services a framework provides. Comfort isn’t always a bad thing but by not writing these base services in a long time or perhaps never before we tend to forget the fundamentals as to how they work. If you were tasked to write a web-app in native PHP without the use of any frameworks or composer packages could you? Short of a coding challenge for a job interview, you should always have access to frameworks making this a non issue. But if you answered no to the question before, perhaps a quick exercise may help to sharpen our knowledge and grow our appreciation for all the developers who contribute their time making our frameworks so great to work with.

The Code

Most routing implementations can be broken down into registering actions and dispatching actions. We register our various routing actions, and then once all routes are registered we execute our dispatch which processes the incoming website url and matches it to a function to call in our code. While routing logic can be made complex, I’ve done my best to provide a stripped down router that best works for me, providing an easy way to register routes without all the extra fluff that can make the codebase intimidating.

In order to get dynamic routing to work you’ll need to configure your web server to allow rewrite rules to point to your index.php file. If you are using Apache ensure .htaccess override rules are configured for your server and simply use the following rules for your .htaccess file. If you are using NGINX or any other web server you may have to do some additional research.

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.php$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.php [L]
</IfModule>

Now that we have all our prep work out of the way we can take a look at our simple router class. All we have to do is instantiate the object and call add_route passing a string for action (our url) and a closure as a callback, this is simply the function we wanted called. After all routes have been added simply call the dispatch function and that’s it! The next example should be more clear.

<?php

class Router {

    /**
     * Holds the registered routes
     *
     * @var array $routes
     */
    protected $routes = [];

    /**
     * Register a new route.
     * 
     * @param string   $action   Name of route.
     * @param \Closure $callback Callback function.
     */
    public function add_route($action, $callback)
    {
        $action = trim($action, '/');
        $this->routes[$action] = $callback;
    }

    /**
     * Dispatch the router
     */
    public function dispatch()
    {
        $action   = trim($_SERVER['REQUEST_URI'], '/');
        //Cleanup action in case there are any URL parameters.
        if ( false !== strpos($action, "?") ) {
            $action   = substr($action, 0, strpos($action, "?"));
        }
        $callback = $this->routes[$action];
        call_user_func($callback);
    }
    
}

All we do is require the router class and instantiate the router object. From there we can pass as many routes as we want. The callback can be an in-line function like seen below, or we can use it to call a function on an object if you want to build a MVC architecture. After that we simply build a quick menu to allow us to jump between routes to test it out!

<?php
require_once __DIR__ . '/src/router.php';

$router = new Router();

$router->add_route('/', function () {
  echo 'Home Page';
});

$router->add_route('/about', function () {
  echo 'About Page';
});

$router->dispatch();

?>

<ul>
  <li>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </li>
</ul>

Using Controllers

If you want to have more complex routing then simply create a controller class and call their functions in your routing closure.

    /**
     * Register all nescessary routes.
     *
     * @author Scott Anderson <[email protected]>
     *
     * @return void
     */
    protected function register_routes() {

        $this->router->add_route('/', function () {
            $this->transaction_controller->index();
        });

        $this->router->add_route('/users', function () {
            $this->user_controller->index();
        });

        $this->router->add_route('/view', function () {
            $this->user_controller->view();
        });

        $this->router->add_route('/migrate', function () {
            $this->user_controller->migrate();
        });

        $this->router->dispatch();
    }

Conclusion

While it may be rare for you to write your own custom router logic its good to understand the fundamentals as to how it can be done. If there is a takeaway I hope you learned routing logic doesn’t have to be complex but be thankful for all the developer before you who make the frameworks we use daily so great to program in.