Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b923553595 | ||
![]() |
1c58ff3a59 | ||
![]() |
1eed623c18 | ||
![]() |
34b4e4ca20 | ||
![]() |
4f5f2599f5 | ||
![]() |
393022b342 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
||||
tests/trees
|
||||
|
||||
/vendor/
|
||||
index.php
|
||||
|
@ -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.
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sharkk/router",
|
||||
"description": "A simple node-based router.",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.4",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
|
185
src/Router.php
185
src/Router.php
@ -1,185 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
class Router
|
||||
{
|
||||
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)
|
||||
*
|
||||
* Example:
|
||||
* `$r->add('GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
|
||||
*/
|
||||
public function add(string $method, string $route, callable $handler): Router
|
||||
{
|
||||
// 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, '/')));
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Add the handler to the last node
|
||||
$node[$method] = $handler;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 405 for method not allowed.
|
||||
*
|
||||
* @return array ['code', 'handler', 'params']
|
||||
*/
|
||||
public function lookup(string $method, string $uri): array
|
||||
{
|
||||
// normalize uri to be tolerant of trailing slashes
|
||||
$uri = '/' . trim($uri, '/');
|
||||
|
||||
// node is a reference to our current location in the node tree
|
||||
$node = $this->routes;
|
||||
|
||||
// init the response array
|
||||
$res = ['code' => 0, 'handler' => null, 'params' => []];
|
||||
|
||||
// if the URI is just a slash, we can return the handler for the root node
|
||||
if ($uri === '/') {
|
||||
if (!$this->checkForHandlers($node)) {
|
||||
$res['code'] = 404;
|
||||
return $res;
|
||||
}
|
||||
|
||||
if (isset($node[$method])) {
|
||||
$res['code'] = 200;
|
||||
$res['handler'] = $node[$method];
|
||||
} else {
|
||||
$res['code'] = 405;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
// we'll split up the URI into segments and traverse the node tree
|
||||
foreach (explode('/', trim($uri, '/')) as $segment) {
|
||||
// check that the next segment is an array (not a callable) and exists, then move to it
|
||||
if (isset($node[$segment]) && is_array($node[$segment])) {
|
||||
$node = $node[$segment];
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there is a dynamic segment, move to it and store the value
|
||||
if (isset($node[':x'])) {
|
||||
$res['params'][] = $segment;
|
||||
$node = $node[':x'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we can't find a node for this segment, return 404
|
||||
$res['code'] = 404;
|
||||
return $res;
|
||||
}
|
||||
|
||||
// if no handlers exist at this node, it's not a valid endpoint - return 404
|
||||
if (!$this->checkForHandlers($node)) {
|
||||
$res['code'] = 404;
|
||||
return $res;
|
||||
}
|
||||
|
||||
// if we found a handler for the method, return it and any params. if not, return a 405
|
||||
if (isset($node[$method])) {
|
||||
$res['code'] = 200;
|
||||
$res['handler'] = $node[$method];
|
||||
} else {
|
||||
$res['code'] = 405;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all routes from the router.
|
||||
*/
|
||||
public function clear(): Router
|
||||
{
|
||||
$this->routes = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the route tree as an array.
|
||||
*/
|
||||
public function dump(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given node has any method handlers.
|
||||
*/
|
||||
private function checkForHandlers(array $node): bool
|
||||
{
|
||||
foreach (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as $m)
|
||||
if (isset($node[$m]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a GET route.
|
||||
*/
|
||||
public function get(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('GET', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route.
|
||||
*/
|
||||
public function post(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('POST', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route.
|
||||
*/
|
||||
public function put(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('PUT', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PATCH route.
|
||||
*/
|
||||
public function patch(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('PATCH', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route.
|
||||
*/
|
||||
public function delete(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('DELETE', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a HEAD route.
|
||||
*/
|
||||
public function head(string $route, callable $handler): Router
|
||||
{
|
||||
return $this->add('HEAD', $route, $handler);
|
||||
}
|
||||
}
|
9
src/RouterInterface.php
Normal file
9
src/RouterInterface.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
interface RouterInterface
|
||||
{
|
||||
public function add(string $method, string $route, callable $handler): RouterInterface;
|
||||
public function lookup(string $method, string $uri): array;
|
||||
}
|
189
src/SegmentRouter.php
Normal file
189
src/SegmentRouter.php
Normal file
@ -0,0 +1,189 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
trait SegmentRouterTrait
|
||||
{
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* Example:
|
||||
* `$r->add('GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
|
||||
*/
|
||||
public function add(string $method, string $route, callable $handler): RouterInterface
|
||||
{
|
||||
// Recursively build the node tree
|
||||
$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
|
||||
);
|
||||
|
||||
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
|
||||
* 405 for method not allowed.
|
||||
*
|
||||
* @return array ['code', 'handler', 'params']
|
||||
*/
|
||||
public function lookup(string $method, string $uri): array
|
||||
{
|
||||
// node is a reference to our current location in the node tree
|
||||
$node = $this->routes;
|
||||
|
||||
// if the URI is just a slash, we can return the handler for the root node
|
||||
if ($uri === '/') {
|
||||
return isset($node[$method])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => null]
|
||||
: ['code' => 405, 'handler' => null, 'params' => null];
|
||||
}
|
||||
|
||||
// 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' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all routes from the router.
|
||||
*/
|
||||
public function clear(): RouterInterface
|
||||
{
|
||||
$this->routes = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the route tree as an array.
|
||||
*/
|
||||
public function dump(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a GET route.
|
||||
*/
|
||||
public function get(string $route, callable $handler): RouterInterface
|
||||
{
|
||||
return $this->add('GET', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route.
|
||||
*/
|
||||
public function post(string $route, callable $handler): RouterInterface
|
||||
{
|
||||
return $this->add('POST', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route.
|
||||
*/
|
||||
public function put(string $route, callable $handler): RouterInterface
|
||||
{
|
||||
return $this->add('PUT', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PATCH route.
|
||||
*/
|
||||
public function patch(string $route, callable $handler): RouterInterface
|
||||
{
|
||||
return $this->add('PATCH', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route.
|
||||
*/
|
||||
public function delete(string $route, callable $handler): RouterInterface
|
||||
{
|
||||
return $this->add('DELETE', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a HEAD route.
|
||||
*/
|
||||
public function head(string $route, callable $handler): 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;
|
||||
}
|
@ -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))));
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once 'color.php';
|
||||
require_once __DIR__ . '/../src/Router.php';
|
||||
require_once __DIR__ . '/../src/RouterInterface.php';
|
||||
require_once __DIR__ . '/../src/SegmentRouter.php';
|
||||
|
||||
const ROUTES = __DIR__ . '/routes/';
|
||||
const TREES = __DIR__ . '/trees/';
|
||||
|
||||
use Sharkk\Router\Router;
|
||||
use Sharkk\Router\SegmentRouter;
|
||||
|
||||
// if there's a flag, reset the opcache
|
||||
if (in_array('-f', $argv)) {
|
||||
@ -17,7 +18,7 @@ if (in_array('-f', $argv)) {
|
||||
// create the trees directory if it doesn't exist
|
||||
if (!is_dir(TREES)) mkdir(TREES);
|
||||
|
||||
$r = new Router();
|
||||
$r = new SegmentRouter();
|
||||
|
||||
// Blog lookups
|
||||
$blog = readAndAddRoutes(ROUTES . 'blog.txt', $r);
|
||||
@ -67,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)];
|
||||
@ -117,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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user