The Goal: Handling Dynamic URLs

Our current router is great, but it has one major limitation: it only matches exact, static paths like /. Real-world applications need to handle dynamic URLs where parts of the path are variables. For example:

  • /posts/my-first-post - where "my-first-post" is a dynamic slug.
  • /users/123 - where "123" is a dynamic user ID.

In this lesson, we will upgrade our router to handle these dynamic segments using Regular Expressions (Regex).


Step 1: Upgrading the Router Class

We need to completely rewrite our dispatch() method. The new version will convert our route paths (like /post/{slug}) into a Regex pattern that can be used to match against the incoming URL and extract the dynamic parts.

Replace the entire content of app/Core/Router.php with this new code:


<?php

namespace App\Core;

class Router
{
    protected $routes = [];
    protected $params = [];

    public function add($route, $controller, $method)
    {
        // Convert the route to a regular expression: escape forward slashes
        $route = preg_replace('/\//', '\\/', $route);

        // Convert variables e.g. {slug}
        $route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z0-9-]+)', $route);

        // Add start and end delimiters, and case-insensitive flag
        $route = '/^' . $route . '$/i';

        $this->routes[$route] = ['controller' => $controller, 'method' => $method];
    }

    public function dispatch()
    {
        $uri = strtok($_SERVER['REQUEST_URI'], '?');
        
        foreach ($this->routes as $route => $params) {
            if (preg_match($route, $uri, $matches)) {
                // Remove the full URL match
                array_shift($matches); 
                
                $this->params = $matches;

                $controller = "App\\Controllers\\" . $params['controller'];
                $method = $params['method'];

                if (class_exists($controller)) {
                    $controllerInstance = new $controller();

                    if (method_exists($controllerInstance, $method)) {
                        call_user_func_array([$controllerInstance, $method], $this->params);
                        return; // Stop searching once a match is found
                    }
                }
            }
        }
        
        // If no route was matched
        http_response_code(404);
        echo "404 Not Found - No route matched for URI: {$uri}";
    }
}

How does the new code work?

  • The add() method now converts a simple path like /post/{slug} into a complex Regex pattern like /^\/post\/(?P<slug>[a-z0-9-]+)$/i. This looks scary, but it's just a precise way of defining a URL pattern.
  • The dispatch() method loops through these Regex patterns and uses preg_match to find a match. If it finds one, it extracts the value of the placeholder (e.g., "my-first-post") and passes it as an argument to the controller method.

Step 2: Creating a PostController to Test The New Router

To test our new router, we need a controller that can accept a dynamic parameter. Let's create a PostController with a show() method that accepts a $slug.

Create a new file: app/Controllers/PostController.php


<?php

namespace App\Controllers;

class PostController
{
    /**
     * Show a single post.
     * The $slug parameter is automatically passed by our Router.
     */
    public function show($slug)
    {
        // For now, we'll just display the slug to prove it's working.
        // In the next lesson, we'll use this to fetch data from the database.
        echo "This is the show method of PostController. ";
        echo "The requested slug is: " . htmlspecialchars($slug);
    }
}

Step 3: Registering the Dynamic Route

Finally, let's tell our application about this new dynamic route in our main entry point file.

Update your file: public/index.php


<?php

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../vendor/autoload.php';

use App\Core\Router;
use App\Core\App;

App::bind('database', $pdo);

$router = new Router();

// Register routes
$router->add('/', 'HomeController', 'index');
$router->add('/post/{slug}', 'PostController', 'show'); // Our new dynamic route!

// Dispatch the router
$router->dispatch();

Your Mission

  1. Replace the entire content of app/Core/Router.php with the new, upgraded code.
  2. Create the new controller file: app/Controllers/PostController.php.
  3. Update public/index.php to register the new /post/{slug} route.
  4. Test it! Open your browser and navigate to a URL like /post/hello-world or /post/my-awesome-post-123. You should see the message from the PostController, confirming that the slug was successfully captured from the URL.

With this upgrade, our router is now significantly more powerful and can handle most of the routing needs of a typical web application.