From 393022b342f1d342cc470ca82304a409e3acf355 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Wed, 25 Dec 2024 10:09:30 -0500 Subject: [PATCH 1/6] Tweaks and optimizations --- README.md | 4 ++-- src/SegmentRouter.php | 35 +++++++++++++++++++++++++------- tests/makeRoutes.php | 46 +++++++++++++++++-------------------------- tests/test.php | 18 +++++------------ 4 files changed, 53 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 04a123c..2834ef6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Hey there! This repo is an experiment to create a well-tuned and speedy URI rout - It's fast - It's simple -Prior to this proejct, I built the [SimpleRouter](https://github.com/splashsky/simplerouter) as a fork from a very cool project called the simplePHPRouter. That router was based on regex for parameterization, and the regex made it pretty painful to maintain if I ever left it alone for too long. +Prior to this project, I built the [SimpleRouter](https://github.com/splashsky/simplerouter) as a fork from a very cool project called the simplePHPRouter. That router was based on regex for parameterization, and the regex made it pretty painful to maintain if I ever left it alone for too long. Since working on SimpleRouter, I've played with other languages (primarily Go) and found a few new ways of doing things. @@ -23,7 +23,7 @@ Take for example these routes: A radix (and more specifically, a PATRICIA) trie takes the commonalities in these routes and makes them into nodes, or branches. `/` exists as the root node. `api/` is turned into a node, from which `v1/` and `/v2/no` branch. `hello` is taken as another branch with the `/` and `:param` child nodes. `/foo` is naturally it's only branch from the root. -By splitting these routes up into a trie based on their segments, you're able to iterate far more quickly through the tree to find what you're looking for. If a user then requests `/api/v1/hello/sky` the router can jump from the root, to `api/`, to `v/1`, to `hello/`, then to the final node much faster than if we had to chop up, say, an associative array and compare for every registered route. +By splitting these routes up into a trie based on their segments, you're able to iterate far more quickly through the tree to find what you're looking for. If a user then requests `/api/v1/hello/sky` the router can jump from the root, to `api/`, to `v1/`, to `hello/`, then to the final node much faster than if we had to chop up, say, an associative array and compare for every registered route. The nodes can contain any arbitrary information, such as HTTP methods or handlers. From my experience, this method of lookup prefers specificity, and so it will always prefer the edges over the inner structures. diff --git a/src/SegmentRouter.php b/src/SegmentRouter.php index 69651d8..f0bc6ef 100644 --- a/src/SegmentRouter.php +++ b/src/SegmentRouter.php @@ -20,13 +20,8 @@ class SegmentRouter implements RouterInterface return str_starts_with($segment, ':') ? ':x' : $segment; }, explode('/', trim($route, '/'))); - // Push each segment into the routes array as a node, except if this is the root node - $node = &$this->routes; - foreach ($segments as $segment) { - // skip an empty segment, which allows us to register handlers for the root node - if ($segment === '') continue; - $node = &$node[$segment]; // build the node tree as we go - } + // Recursively build the node tree + $this->addNode($this->routes, $segments, $method, $handler); // Add the handler to the last node $node[$method] = $handler; @@ -34,6 +29,32 @@ class SegmentRouter implements RouterInterface return $this; } + private function addNode(array &$node, array $segments, string $method, callable $handler): void + { + // Base case: if no more segments, add the handler + if (empty($segments)) { + $node[$method] = $handler; + return; + } + + // Get the current segment + $segment = array_shift($segments); + + // Skip empty segments + if ($segment === '') { + $this->addNode($node, $segments, $method, $handler); + return; + } + + // Ensure the segment exists in the node + if (!isset($node[$segment])) { + $node[$segment] = []; + } + + // Recur for the next segment + $this->addNode($node[$segment], $segments, $method, $handler); + } + /** * Perform a lookup in the route tree for a given method and URI. Returns an array with a result code, * a handler if found, and any dynamic parameters. Codes are 200 for success, 404 for not found, and diff --git a/tests/makeRoutes.php b/tests/makeRoutes.php index f390609..3955f2d 100644 --- a/tests/makeRoutes.php +++ b/tests/makeRoutes.php @@ -8,38 +8,28 @@ $params = ['id', 'slug', 'page', 'extra', 'foo', 'string', 'number', 'bar', 'baz $endpoint = ['edit', 'create', 'delete', 'view', 'change', 'modify', 'generate', 'lift', 'lower', 'raise', 'drop', 'pick', 'choose', 'select', 'deselect', 'unselect', 'reselect', 'pick', 'unpick', 'repick', 'reselect', 'reunpick', 'rechoose', 'reselect']; $midpoint = ['do', 'cause', 'effect', 'affect', 'impact', 'influence', 'change', 'modify', 'transform', 'alter', 'shift', 'adjust', 'adapt', 'convert', 'translate']; -// Generate routes -for ($i = 0; $i < 1000; $i++) { - $routes[] = makeRoute(); - // write the routes array to a file - file_put_contents('routes/big.txt', implode("\n", $routes)); +file_put_contents('routes/big.txt', implode("\n", array_map(fn() => makeRoute(), range(1, 1000)))); + +enum RoutePart: string { + case PARAMS = 'params'; + case ENDPOINT = 'endpoint'; + case MIDPOINT = 'midpoint'; + + public function matches(array $params, array $endpoint, array $midpoint): string { + return '/' . match ($this) { + self::PARAMS => ':' . $params[array_rand($params)], + self::ENDPOINT => $endpoint[array_rand($endpoint)], + self::MIDPOINT => $midpoint[array_rand($midpoint)], + }; + } } function makeRoute(): string { global $methods, $apis, $params, $endpoint, $midpoint; - $method = $methods[array_rand($methods)]; - $api = $apis[array_rand($apis)]; - $route = "/$api"; - $length = rand(1, 8); - $options = ['params', 'endpoint', 'midpoint']; - for ($i = 0; $i < $length; $i++) { - $option = $options[array_rand($options)]; - switch ($option) { - case 'params': - $param = $params[array_rand($params)]; - $route .= "/:$param"; - break; - case 'endpoint': - $end = $endpoint[array_rand($endpoint)]; - $route .= "/$end"; - break; - case 'midpoint': - $mid = $midpoint[array_rand($midpoint)]; - $route .= "/$mid"; - break; - } - } - return $method . ' ' . $route; + return $methods[array_rand($methods)] . + ' /' . + $apis[array_rand($apis)] . + implode('', array_map(fn($option) => $option->matches($params, $endpoint, $midpoint), array_rand(array_flip(RoutePart::cases()), rand(1, 8)))); } diff --git a/tests/test.php b/tests/test.php index 7bef9d9..1ea63b1 100644 --- a/tests/test.php +++ b/tests/test.php @@ -68,10 +68,7 @@ $routes = [ }], ]; -foreach ($routes as $route) { - [$method, $path, $handler] = $route; - $r->add($method, $path, $handler); -} +array_walk($routes, fn($route) => $r->add($route[0], $route[1], $route[2])); for ($i = 0; $i < 10; $i++) { [$method, $uri] = $routes[array_rand($routes)]; @@ -118,17 +115,12 @@ function echoTitle(string $title) { function readAndAddRoutes(string $file, &$r): array { - $array = []; - $routes = file($file); - foreach ($routes as $route) { + return array_map(function($route) use ($r) { [$method, $path] = explode(' ', $route); $path = trim($path); - $array[] = [$method, $path]; - $r->add($method, $path, function() { - return true; - }); - } - return $array; + $r->add($method, $path, fn() => true); + return [$method, $path]; + }, file($file)); } function runIterations(int $iterations, $r, array $routes) { -- 2.45.1 From 4f5f2599f5fc6da840f463074e8131b4a66e129b Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Wed, 25 Dec 2024 10:22:33 -0500 Subject: [PATCH 2/6] Update SegmentRouter.php --- src/SegmentRouter.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/SegmentRouter.php b/src/SegmentRouter.php index f0bc6ef..a149d33 100644 --- a/src/SegmentRouter.php +++ b/src/SegmentRouter.php @@ -15,13 +15,16 @@ class SegmentRouter implements RouterInterface */ public function add(string $method, string $route, callable $handler): RouterInterface { - // Expand the route into segments and make dynamic segments into a common placeholder - $segments = array_map(function($segment) { - return str_starts_with($segment, ':') ? ':x' : $segment; - }, explode('/', trim($route, '/'))); - // Recursively build the node tree - $this->addNode($this->routes, $segments, $method, $handler); + $this->addNode( + $this->routes, + array_map( // Expand the route into segments and make dynamic segments into a common placeholder + fn($segment) => str_starts_with($segment, ':') ? ':x' : $segment, + explode('/', trim($route, '/')) + ), + $method, + $handler + ); // Add the handler to the last node $node[$method] = $handler; -- 2.45.1 From 34b4e4ca20f84c32e59237be0f20aea279a510b5 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Wed, 25 Dec 2024 10:38:35 -0500 Subject: [PATCH 3/6] Optimize $params using array_reduce --- src/SegmentRouter.php | 48 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/SegmentRouter.php b/src/SegmentRouter.php index a149d33..c22b63c 100644 --- a/src/SegmentRouter.php +++ b/src/SegmentRouter.php @@ -70,9 +70,6 @@ class SegmentRouter implements RouterInterface // node is a reference to our current location in the node tree $node = $this->routes; - // params will hold any dynamic segments we find - $params = []; - // if the URI is just a slash, we can return the handler for the root node if ($uri === '/') { return isset($node[$method]) @@ -80,29 +77,34 @@ class SegmentRouter implements RouterInterface : ['code' => 405, 'handler' => null, 'params' => null]; } - // We'll split up the URI into segments and traverse the node tree - foreach (explode('/', trim($uri, '/')) as $segment) { - // if there is a node for this segment, move to it - if (isset($node[$segment])) { - $node = $node[$segment]; - continue; - } - - // if there is a dynamic segment, move to it and store the value - if (isset($node[':x'])) { - $params[] = $segment; - $node = $node[':x']; - continue; - } - - // if we can't find a node for this segment, return 404 - return ['code' => 404, 'handler' => null, 'params' => []]; - } + // params will hold any dynamic segments we find + $params = array_reduce( + explode('/', trim($uri, '/')), + function ($carry, $segment) use (&$node) { + if (isset($node[$segment])) { + $node = $node[$segment]; + } elseif (isset($node[':x'])) { + $carry[] = $segment; + $node = $node[':x']; + } else { + throw new \Exception('404'); + } + return $carry; + }, + [] + ); // if we found a handler for the method, return it and any params. if not, return a 405 return isset($node[$method]) - ? ['code' => 200, 'handler' => $node[$method], 'params' => $params ?? []] - : ['code' => 405, 'handler' => null, 'params' => []]; + ? [ + 'code' => 200, + 'handler' => $node[$method], + 'params' => $params ?? []] + : [ + 'code' => 405, + 'handler' => null, + 'params' => [] + ]; } /** -- 2.45.1 From 1eed623c183cb7bc32496bfc570f14ebadd6f152 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Wed, 25 Dec 2024 10:50:49 -0500 Subject: [PATCH 4/6] Strict types --- src/RouterInterface.php | 2 +- src/SegmentRouter.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RouterInterface.php b/src/RouterInterface.php index 5488e6f..cd1185c 100644 --- a/src/RouterInterface.php +++ b/src/RouterInterface.php @@ -1,4 +1,4 @@ - Date: Wed, 25 Dec 2024 10:59:20 -0500 Subject: [PATCH 5/6] Trait, Interface, Class --- src/SegmentRouter.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/SegmentRouter.php b/src/SegmentRouter.php index 6974837..f3424cd 100644 --- a/src/SegmentRouter.php +++ b/src/SegmentRouter.php @@ -2,10 +2,8 @@ namespace Sharkk\Router; -class SegmentRouter implements RouterInterface +trait SegmentRouterTrait { - private array $routes = []; - /** * Add a route to the route tree. The route must be a URI path, and contain dynamic segments * using a colon prefix. (:id, :slug, etc) @@ -172,3 +170,23 @@ class SegmentRouter implements RouterInterface return $this->add('HEAD', $route, $handler); } } + +interface SegmentRouterInterface extends RouterInterface +{ + //private function addNode(array &$node, array $segments, string $method, callable $handler): void; + public function clear(): RouterInterface; + public function dump(): array; + public function get(string $route, callable $handler): RouterInterface; + public function post(string $route, callable $handler): RouterInterface; + public function put(string $route, callable $handler): RouterInterface; + public function patch(string $route, callable $handler): RouterInterface; + public function delete(string $route, callable $handler): RouterInterface; + public function head(string $route, callable $handler): RouterInterface; +} + +class SegmentRouter implements SegmentRouterInterface +{ + private array $routes = []; + + use SegmentRouterTrait; +} -- 2.45.1 From b923553595af544e206115eea62a2b486916f990 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Thu, 26 Dec 2024 08:36:08 -0500 Subject: [PATCH 6/6] Remove used assignment $node didn't even exist anymore --- src/SegmentRouter.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SegmentRouter.php b/src/SegmentRouter.php index f3424cd..3e304db 100644 --- a/src/SegmentRouter.php +++ b/src/SegmentRouter.php @@ -24,9 +24,6 @@ trait SegmentRouterTrait $handler ); - // Add the handler to the last node - $node[$method] = $handler; - return $this; } -- 2.45.1