Implementing a PHP Router with Optional Parameters

49 Views Asked by At

I've been working on implementing a simple PHP router and have managed to make it work with required parameters. However, I'm struggling to implement optional parameters but I failed. I'll provide the relevant code for clarity.

Firstly, this is my index.php:

<?php
$app->route->get("/user/:id/?post_id", [SiteController::class, "contact"]);
?>

In the above code, I've used a colon (:id) for required parameters and a question mark (?post_id) for optional parameters.

Secondly, here is my Router class:

class Router {
    public function resolve() {
        $method = $this->request->getMethod();
        $url = $this->request->getUrl();
        foreach ($this->router[$method] as $routeUrl => $target) {
            $pattern = preg_replace('/\/:([^\/]+)/', '/(?P<$1>[^/]+)', $routeUrl);
            if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
                $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
                call_user_func([new $target[0], $target[1]], ...array_values($params));
                exit();
            }
        }
        throw new \Exception();
    }
}

I need assistance in solving the mystery of implementing optional parameters. Any help would be greatly appreciated. Thank you!

1

There are 1 best solutions below

0
ThW On

Here is a function that compiles a route path pattern into a regular expression with named capture groups. It uses a callback function to return different patterns depending on the indicator character.

function compileRoutePattern(string $route): string {
    // replace parameter syntax with named capture groups
    return '(^'.preg_replace_callback(
      '((?<prefix>^|/)(?:(?<indicator>[:?])(?<name>[\w\d]+)|[^/]+))',
      function($match) {
          return match ($match['indicator'] ?? '') {
               // mandatory parameter 
              ':' => sprintf('(?:%s(?<%s>[^/]+))', $match['prefix'], $match['name']),
               // optional parameter 
              '?' => sprintf('(?:%s(?<%s>[^/]+))?', $match['prefix'], $match['name']),
              // escape anything else
              default => preg_quote($match[0], '(')
          };
      },
      $route
    ).')i';
}

The next step would be to match a path and return only the named capture groups (not the numeric keys).

function matchRoute(string $path, string $route): ?array {
    // compile route into regex with named capture groups
    $pattern = compileRoutePattern($route);
    // match
    if (preg_match($pattern, $path, $match)) {
        // filter numeric keys from match
        return array_filter(
            $match, 
            fn($key) => is_string($key),
            ARRAY_FILTER_USE_KEY
        );   
    }
    return null;
}

Hint: I am using () as pattern delimiters to avoid conflicts. Think of them as group 0.