<?php /* * Copyright (c) 2022. Micha Espey <tracer@24unix.net> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * */ namespace App\Service; use App\Entity\Route; use Closure; /* * A small router implementation for the address book demo. * Currently it doesn't handle GET requests, as not needed. * But if I reuse the code in my bindApi I'll maybe support GET as well. */ class Router { /* * The easiest way to differentiate between static and dynamic routes is using * two arrays, no need to pollute the class Route with that information */ private array $staticRoutes = []; private array $dynamicRoutes = []; public function __construct(private readonly Template $template) { // empty body } /* * This method takes a route like /admin/users/{user} and creates a regex to match on call * More complex routes as /posts/{thread}/show/{page} are supported as well. */ function addRoute(string $name, string $route, Closure $callback): void { // check for parameters preg_match_all(pattern: "/(?<={).+?(?=})/", subject: $route, matches: $matches); $parameters = $matches[0]; // create regex for route: $regex = preg_replace(pattern: '/{.+?}/', replacement: '([a-zA-Z0-9]*)', subject: $route); $regex = '/^' . str_replace(search: "/", replace: '\\/', subject: $regex) . '$/i'; $route = new Route(name: $name, route: $route, regEx: $regex, parameters: $parameters, callback: $callback); if ($parameters) { $this->dynamicRoutes[] = $route; } else { $this->staticRoutes[] = $route; } } /* * Checks if there is a known route and executes the callback. */ public function handleRouting(): void { $requestUri = $_SERVER['REQUEST_URI']; /* * Static routes have preference over dynamic ones, so * /admin/user/add to add and * /admin/user/{name} to edit is possible. * A user named "add" of course not :) * * But who wants to call their users "add" or "delete"? * That's as weird as Little Bobby Tables … (https://xkcd.com/327/) */ foreach ($this->staticRoutes as $route) { if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) { call_user_func(callback: $route->getCallback()); // We've found our route, bail out. return; } } foreach ($this->dynamicRoutes as $route) { $parameters = []; if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) { foreach ($route->getParameters() as $id => $parameter) { $parameters[$parameter] = $matches[$id + 1]; } // PHP is mad about named parameters in call_user_func when adding parameters. // Uncaught Error: Unknown named parameter $args in <sourceFile> // But PHPStorm seems happy without them. So what? call_user_func($route->getCallback(), $parameters); return; } } // if no route is matched, throw a 404 $this->template->render(templateName: 'status/404.html.php'); } public function path(string $routeName, array $vars = []) { foreach (array_merge($this->dynamicRoutes, $this->staticRoutes) as $route) { if ($route->getName() == $routeName) { if ($vars) { // build route $route = $route->getRoute(); // replace placeholder with current values foreach ($vars as $key => $value) { $route = str_replace(search: '{' . $key . '}', replace: $value, subject: $route); } return $route; } else { return $route->getRoute(); } } } // no 404, this is reached only if the code is wrong die("Missing Route: $routeName"); } }