Compare commits

..

6 Commits

Author SHA1 Message Date
Valithor Obsidion
b923553595 Remove used assignment
$node didn't even exist anymore
2024-12-26 08:36:08 -05:00
Valithor Obsidion
1c58ff3a59 Trait, Interface, Class 2024-12-25 10:59:20 -05:00
Valithor Obsidion
1eed623c18 Strict types 2024-12-25 10:50:49 -05:00
Valithor Obsidion
34b4e4ca20 Optimize $params using array_reduce 2024-12-25 10:46:16 -05:00
Valithor Obsidion
4f5f2599f5 Update SegmentRouter.php 2024-12-25 10:22:33 -05:00
Valithor Obsidion
393022b342 Tweaks and optimizations 2024-12-25 10:09:30 -05:00
5 changed files with 109 additions and 86 deletions

View File

@ -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 fast
- It's simple - 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. 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. 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. 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.

View File

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
namespace Sharkk\Router; namespace Sharkk\Router;

View File

@ -1,11 +1,9 @@
<?php <?php declare(strict_types=1);
namespace Sharkk\Router; 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 * 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) * using a colon prefix. (:id, :slug, etc)
@ -15,25 +13,46 @@ class SegmentRouter implements RouterInterface
*/ */
public function add(string $method, string $route, callable $handler): RouterInterface public function add(string $method, string $route, callable $handler): RouterInterface
{ {
// Expand the route into segments and make dynamic segments into a common placeholder // Recursively build the node tree
$segments = array_map(function($segment) { $this->addNode(
return str_starts_with($segment, ':') ? ':x' : $segment; $this->routes,
}, explode('/', trim($route, '/'))); array_map( // Expand the route into segments and make dynamic segments into a common placeholder
fn($segment) => str_starts_with($segment, ':') ? ':x' : $segment,
// Push each segment into the routes array as a node, except if this is the root node explode('/', trim($route, '/'))
$node = &$this->routes; ),
foreach ($segments as $segment) { $method,
// skip an empty segment, which allows us to register handlers for the root node $handler
if ($segment === '') continue; );
$node = &$node[$segment]; // build the node tree as we go
}
// Add the handler to the last node
$node[$method] = $handler;
return $this; 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, * 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 * a handler if found, and any dynamic parameters. Codes are 200 for success, 404 for not found, and
@ -46,9 +65,6 @@ class SegmentRouter implements RouterInterface
// node is a reference to our current location in the node tree // node is a reference to our current location in the node tree
$node = $this->routes; $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 the URI is just a slash, we can return the handler for the root node
if ($uri === '/') { if ($uri === '/') {
return isset($node[$method]) return isset($node[$method])
@ -56,29 +72,34 @@ class SegmentRouter implements RouterInterface
: ['code' => 405, 'handler' => null, 'params' => null]; : ['code' => 405, 'handler' => null, 'params' => null];
} }
// We'll split up the URI into segments and traverse the node tree // params will hold any dynamic segments we find
foreach (explode('/', trim($uri, '/')) as $segment) { $params = array_reduce(
// if there is a node for this segment, move to it explode('/', trim($uri, '/')),
if (isset($node[$segment])) { function ($carry, $segment) use (&$node) {
$node = $node[$segment]; if (isset($node[$segment])) {
continue; $node = $node[$segment];
} } elseif (isset($node[':x'])) {
$carry[] = $segment;
// if there is a dynamic segment, move to it and store the value $node = $node[':x'];
if (isset($node[':x'])) { } else {
$params[] = $segment; throw new \Exception('404');
$node = $node[':x']; }
continue; return $carry;
} },
[]
// if we can't find a node for this segment, return 404 );
return ['code' => 404, 'handler' => null, 'params' => []];
}
// if we found a handler for the method, return it and any params. if not, return a 405 // if we found a handler for the method, return it and any params. if not, return a 405
return isset($node[$method]) 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' => []
];
} }
/** /**
@ -146,3 +167,23 @@ class SegmentRouter implements RouterInterface
return $this->add('HEAD', $route, $handler); 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;
}

View File

@ -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']; $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']; $midpoint = ['do', 'cause', 'effect', 'affect', 'impact', 'influence', 'change', 'modify', 'transform', 'alter', 'shift', 'adjust', 'adapt', 'convert', 'translate'];
// Generate routes file_put_contents('routes/big.txt', implode("\n", array_map(fn() => makeRoute(), range(1, 1000))));
for ($i = 0; $i < 1000; $i++) {
$routes[] = makeRoute(); enum RoutePart: string {
// write the routes array to a file case PARAMS = 'params';
file_put_contents('routes/big.txt', implode("\n", $routes)); 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 function makeRoute(): string
{ {
global $methods, $apis, $params, $endpoint, $midpoint; global $methods, $apis, $params, $endpoint, $midpoint;
$method = $methods[array_rand($methods)]; return $methods[array_rand($methods)] .
$api = $apis[array_rand($apis)]; ' /' .
$route = "/$api"; $apis[array_rand($apis)] .
$length = rand(1, 8); implode('', array_map(fn($option) => $option->matches($params, $endpoint, $midpoint), array_rand(array_flip(RoutePart::cases()), 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;
} }

View File

@ -68,10 +68,7 @@ $routes = [
}], }],
]; ];
foreach ($routes as $route) { array_walk($routes, fn($route) => $r->add($route[0], $route[1], $route[2]));
[$method, $path, $handler] = $route;
$r->add($method, $path, $handler);
}
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
[$method, $uri] = $routes[array_rand($routes)]; [$method, $uri] = $routes[array_rand($routes)];
@ -118,17 +115,12 @@ function echoTitle(string $title) {
function readAndAddRoutes(string $file, &$r): array function readAndAddRoutes(string $file, &$r): array
{ {
$array = []; return array_map(function($route) use ($r) {
$routes = file($file);
foreach ($routes as $route) {
[$method, $path] = explode(' ', $route); [$method, $path] = explode(' ', $route);
$path = trim($path); $path = trim($path);
$array[] = [$method, $path]; $r->add($method, $path, fn() => true);
$r->add($method, $path, function() { return [$method, $path];
return true; }, file($file));
});
}
return $array;
} }
function runIterations(int $iterations, $r, array $routes) { function runIterations(int $iterations, $r, array $routes) {