📖 PHP MVC Routing

Routing is a method of matching URLs to specific controller and actions. This will allow us to manage the URL requests more efficiently including URLs that don't have a matching page.

Our site will likely contain many pages and at times we will want to add more. In order to manage the page load process, we are going to create a route table to organize our page routes. Each valid URL request must be placed in the table as the site will need to know how to build the request page and include the necessary resources (controller, model, view).

Let's look at a sample of possible URL page requests and the matching controller and actions. Remember, at this point in our site development, we need to know which controller (class) to call and which action (method) to run. We need to match each of these to their respective URLs.

Basic URL Controller/action Table
URLControllerAction
/home/index Home index
/products/index Products index
/products/show Products show

This is good for fixed URLs with the controller and action segments in the path, but what about other possible requests? For example, how will the app process a request that only includes the domain name as most sites use? What about processing a URL with specific content information like article or product IDs?

Expanded URL Routes Table
URLControllerAction
/ Home index
/products Products index
/show/123 Products show

We need a more robust way of processing the URL request for more flexibility. We can do this using a routing table. With the route table in place, we can determine which controller/action to call for each URL request even if the controller and action are not explicitly stated in the URL.

Add the router.php File

To add this functionality to our site, we add a new file in the src directory named router.php. In the router.php file we will add the Route class with a method to map the requested URL to the routes available in the site. Notice the model, view and controller files are collapsed to avoid confusion about the placement of the router.php file. The router.php file should be placed in the src directory.

File system with router.php file

The Router class

We continue building the Router class definition in the router.php file. Notice that for each route we added, we will call the add() method from the $router object. This needs to be added to the Router class.

Begin by creating a private array $routes in the Router class to store the routes from the router table located in the index.php file. Then create a public function called add() with a string argument called $path and an array argument called $params to match the routes in the index.php file. When we call the add() method, we will pass the $path and $params to the add() method then add them to the $routes[] array.

<?php

class Router
{
    // create array to hold routes from route table
    private array $routes = [];

    // create method to add routes from the route table to the routes array
    public function add(string $path, array $params): void
    {
        $this->routes[] = [
            "path" => $path,
            "params" => $params
        ];
    }
}

Update the index.php File

We need to include the router.php file in the index.php file along with a call to create a new Router from the Router class in the router.php file. This will connect our router table to the Router class to process the requested URLs. After we include the router file and create a new Router object from the Router class, we will begin defining our routes.

<?php

// include the router file with the Router class definition
require "src/router.php";

// create a new Router object from the Router class
$router = new Router;


// begin adding routes to the router table
$router->add("/", ["controller" => "home", "action" => "index"]);
$router->add("/products", ["controller" => "products", "action" => "index"]);
$router->add("/products/show", ["controller" => "products", "action" => "show"]);

Now that we have some routes in the router table, we can begin matching them to available routes. Add a new public function to the Router class called matchRoute(). This method will take the $path value from the URL and attempt to match it to an available route from the $routes[] array. If matched, it will access the $params. If no match, it returns false. We will call this method in the index.php page.

<?php

class Router
{
    // create array to hold routes from route table
    private array $routes = [];

    // create method to add routes from the route table to the routes array
    public function add(string $path, array $params): void
    {
        $this->routes[] = [
            "path" => $path,
            "params" => $params
        ];
    }

    // create method to check a URL for a matching route from the routes table
    public function matchRoute(string $path): array|bool
    {
        foreach ($this->routes as $route) {

            if ($route["path"] === $path) {

                return $route["params"];

            }
        }

        return false;
    }
}

The index.php Page

Back in the index.php file, we need to add the matchRoute()  method call to enable route matching.

<?php

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// include the router file with the Router class definition
require "src/router.php";

// create a new Router object from the Router class
$router = new Router;

// begin adding routes to the $routes array
$router->add("/", ["controller" => "home", "action" => "index"]);
$router->add("/products", ["controller" => "products", "action" => "index"]);
$router->add("/products/show", ["controller" => "products", "action" => "show"]);

// call to matchRoute() to return an array of $params from $routes
$params = $router->matchRoute($path);

// test the output in a browser
var_dump($params);
exit;

$segments = explode('/', $path);

$controller = $segments[1];
$action = $segments[2];

require "src/controllers/$controller.php";

$controller_object = new $controller;

$controller_object->$action();
Test the matchRoute() method in a Browser

With the matchRoute() method called in the index.php file, we can test the script and see the output from various URLs. Open a browser and try some valid URLs from the routes table and some that do not exist to see the result.

For valid routes you should see the resulting parameters defined in the routes table.

/products returns array(2) { ["controller"]=> string(8) "products" ["action"]=> string(5) "index" }.

For invalid routes, you should see bool(false).

Use $params to Load the Controller and Action

Now that we have the controller and action parameters from the route table, we can use these to call the appropriate controller and action to build our page. First, remove the test code and the $segments explode() statement we used earlier as we no longer need them to map our URL to a controller/action.

<?php
// remove these lines
...
// test the output in a browser
var_dump($params);
exit;

$segments = explode('/', $path);

// get the controller and action from the query string
$controller = $segments[1];
$action = $segments[2];
...

Now we can add the code to define the $controller and $action variables to call the page load from the new Router class.

<?php

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

require "src/router.php";

$router = new Router;

$router->add("/", ["controller" => "home", "action" => "index"]);
$router->add("/products", ["controller" => "products", "action" => "index"]);
$router->add("/products/show", ["controller" => "products", "action" => "show"]);

$params = $router->matchRoute($path);

// edit these variables to assign values from $params array from Router class
$controller = $params["controller"];
$action = $params["action"];

require "src/controllers/$controller.php";

$controller_object = new $controller;

$controller_object->$action();

You should be able to test for page load on existing routes. For non-existent routes we will simply return a "No matching route" message for now. Since the matchRoute() method returns false if no route is found, we can simply check for a false return from the method call. In the index.php file, add a conditional statement to check for a false return from the matchRoute() method. After you call the matchRoute() and before you assign values to the $controller and $action variables, check for a false return from matchRoute().

<?php
...
$params = $router->matchRoute($path);

// check for non-existent route
if ($params === false) {

    exit("No matching route");

}

$controller = $params["controller"];
$action = $params["action"];
...

You should be able to test for page load on no-existing routes. We will improve this later.

Tutorial

NOTE: The information in this video was correct at the time of production. Some elements may have changed. Please refer to the course syllabus and assignments for current requirements.