Challenge 3 — Solution Task: Extend the Router class to support route parameters, e.g. registering "/posts/{id}" and matching a real URL like "/posts/42" by extracting 42 and passing it as an argument to the controller method. Use a regular expression (Intermediate Chapter 9) to implement the matching. routes[$path] = $handler; } public function dispatch(string $uri) { foreach ($this->routes as $path => $handler) { $pattern = $this->convertToRegex($path); if (preg_match($pattern, $uri, $matches)) { array_shift($matches); // remove the full match at index 0, keep only captured params [$controllerClass, $method] = $handler; $controller = new $controllerClass(); $controller->$method(...$matches); return; } } http_response_code(404); echo "404 Not Found"; } private function convertToRegex(string $path): string { // Replace {id}, {slug}, etc. with a capturing group matching // one or more non-slash characters $pattern = preg_replace('/\{[a-zA-Z_]+\}/', '([^/]+)', $path); return '#^' . $pattern . '$#'; } } // Usage: // $router->get('/posts/{id}', [PostController::class, 'show']); // A request to "/posts/42" now calls PostController->show('42') ?> Output (conceptual — demonstrating the matching logic in isolation): Registered route "/posts/{id}" converts to regex "#^/posts/([^/]+)$#" Matching "/posts/42" against it captures "42" into $matches[1] After array_shift(), $matches becomes ["42"] $controller->show(...["42"]) calls show('42') — the id is now available inside the controller method Notes: - convertToRegex() uses preg_replace() (Intermediate Chapter 9) to turn any {paramName} placeholder into a capturing group ([^/]+), which matches any run of characters that isn't a forward slash - so a route like "/posts/{id}" correctly matches "/posts/42" but not "/posts/42/edit" (which would need its own separate route). - array_shift($matches) removes preg_match()'s own index 0 (the whole matched string), leaving only the actual captured parameter values - exactly the distinction Intermediate Chapter 9's own capturing-groups section explained between $matches[0] and $matches[1] onward. - ...$matches (the splat operator, also used in Challenge 2 of Chapter 2) unpacks the captured values as individual arguments to the controller method - so a route with two parameters, like "/posts/{id}/comments/{commentId}", would correctly call the method with two separate arguments in order. - The original exact-match dispatch() (a simple isset() lookup) had to be replaced with a foreach loop trying each registered route's own regex pattern in turn, since parameterized routes can no longer be matched with a single direct array-key lookup.