forked from PHP/Router
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
a8b0f2a410 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
|||
tests/trees
|
||||
|
||||
/vendor/
|
||||
composer.lock
|
||||
|
|
|
@ -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 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.
|
||||
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.
|
||||
|
||||
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 `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.
|
||||
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.
|
||||
|
||||
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.4",
|
||||
"version": "1.2.5",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
|
@ -15,8 +15,5 @@
|
|||
"email": "email@sharkk.net"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.5"
|
||||
}
|
||||
"minimum-stability": "stable"
|
||||
}
|
||||
|
|
148
src/Router.php
Normal file
148
src/Router.php
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?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
|
||||
{
|
||||
// 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])
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => null]
|
||||
: ['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' => []];
|
||||
}
|
||||
|
||||
// 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(): Router
|
||||
{
|
||||
$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): 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);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
interface RouterInterface
|
||||
{
|
||||
public static function add(array &$routes, string $method, string $route, callable $handler): void;
|
||||
public static function lookup(array &$routes, string $method, string $uri): array;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
class SegmentRouter implements SegmentRouterInterface
|
||||
{
|
||||
use SegmentRouterTrait;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Sharkk\Router;
|
||||
|
||||
interface SegmentRouterInterface extends RouterInterface
|
||||
{
|
||||
//private function addNode(array &$node, array $segments, string $method, callable $handler): void;
|
||||
public static function clear(array &$routes): void;
|
||||
//public static function dump(array &$routes): array; // Deprecated, routes are now passed by reference
|
||||
public static function get(array &$routes, string $route, callable $handler): void;
|
||||
public static function post(array &$routes, string $route, callable $handler): void;
|
||||
public static function put(array &$routes, string $route, callable $handler): void;
|
||||
public static function patch(array &$routes, string $route, callable $handler): void;
|
||||
public static function delete(array &$routes, string $route, callable $handler): void;
|
||||
public static function head(array &$routes, string $route, callable $handler): void;
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
<?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 static function add(array &$routes, string $method, string $route, callable $handler): void
|
||||
{
|
||||
// Recursively build the node tree
|
||||
self::addNode(
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler to the routing node based on the provided segments and method.
|
||||
*
|
||||
* This method recursively traverses the segments array, creating nested nodes as needed,
|
||||
* and assigns the handler to the final node corresponding to the method.
|
||||
*
|
||||
* @param array $node The current routing node.
|
||||
* @param array $segments The segments of the route to add.
|
||||
* @param string $method The HTTP method (e.g., 'GET', 'POST') for which the handler is being added.
|
||||
* @param callable $handler The handler to be executed for the given route and method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static 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 === '') {
|
||||
self::addNode($node, $segments, $method, $handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the segment exists in the node
|
||||
if (!isset($node[$segment])) {
|
||||
$node[$segment] = [];
|
||||
}
|
||||
|
||||
// Recur for the next segment
|
||||
self::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 static function lookup(array &$routes, string $method, string $uri): array
|
||||
{
|
||||
// node is a reference to our current location in the node tree
|
||||
$node = $routes;
|
||||
|
||||
// if the URI is the root, and the method is defined, return the handler
|
||||
if (self::isRootUri($uri, $node, $method)) {
|
||||
return ['code' => 200, 'handler' => $node[$method], 'params' => null];
|
||||
}
|
||||
|
||||
// params will hold any dynamic segments we find
|
||||
$params = [];
|
||||
$segments = explode('/', trim($uri, '/'));
|
||||
if (!self::traverseSegments($node, $segments, $params)) {
|
||||
return ['code' => 404, 'handler' => null, 'params' => []];
|
||||
}
|
||||
|
||||
return self::getHandler($node, $method, $params) ?? ['code' => 405, 'handler' => null, 'params' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the given segments and updates the node and params accordingly.
|
||||
*
|
||||
* This method iterates over the segments and attempts to match each segment
|
||||
* with the corresponding node. If a segment matches, it continues to the next
|
||||
* segment. If a segment does not match but a wildcard ':x' is found, the segment
|
||||
* is added to the params array and traversal continues. If no match is found,
|
||||
* the traversal stops and returns false.
|
||||
*
|
||||
* @param array $node The current node to traverse.
|
||||
* @param array $segments The segments to traverse through the node.
|
||||
* @param array $params The parameters collected during traversal.
|
||||
* @return bool Returns true if traversal is successful, otherwise false.
|
||||
*/
|
||||
private static function traverseSegments(array &$node, array $segments, array &$params): bool
|
||||
{
|
||||
return ($node = array_reduce($segments, function ($carry, $segment) use (&$params) {
|
||||
if (isset($carry[$segment])) {
|
||||
return $carry[$segment];
|
||||
}
|
||||
if (isset($carry[':x'])) {
|
||||
$params[] = $segment;
|
||||
return $carry[':x'];
|
||||
}
|
||||
return false;
|
||||
}, $node)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given URI is the root URI and if the specified method exists in the node array.
|
||||
*
|
||||
* @param string $uri The URI to check.
|
||||
* @param array &$node The node array to check against.
|
||||
* @param string $method The HTTP method to check for in the node array.
|
||||
* @return bool Returns true if the URI is the root URI and the method exists in the node array, false otherwise.
|
||||
*/
|
||||
public static function isRootUri(string $uri, array &$node, string $method): bool
|
||||
{
|
||||
return ($uri === '/' && isset($node[$method]) && array_key_exists($method, $node));
|
||||
}
|
||||
|
||||
// if we found a handler for the method, return it and any params. if not, return a 405
|
||||
|
||||
/**
|
||||
* Retrieves the handler for a given node and method.
|
||||
*
|
||||
* @param array $node The node containing possible handlers.
|
||||
* @param string $method The method to look up in the node.
|
||||
* @param array $params The parameters to pass to the handler.
|
||||
* @return array|null An array containing the handler information if found, or null if not found.
|
||||
*/
|
||||
public static function getHandler($node, $method, $params): ?array
|
||||
{
|
||||
return array_key_exists($method, $node)
|
||||
? ['code' => 200, 'handler' => $node[$method], 'params' => $params]
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all routes from the router.
|
||||
*/
|
||||
public static function clear(array &$routes): void
|
||||
{
|
||||
$routes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the route tree as an array.
|
||||
*/
|
||||
/*public static function dump(array &$routes): array
|
||||
{
|
||||
return $routes;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Register a GET route.
|
||||
*/
|
||||
public static function get(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'GET', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route.
|
||||
*/
|
||||
public static function post(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'POST', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route.
|
||||
*/
|
||||
public static function put(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'PUT', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PATCH route.
|
||||
*/
|
||||
public static function patch(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'PATCH', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route.
|
||||
*/
|
||||
public static function delete(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'DELETE', $route, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a HEAD route.
|
||||
*/
|
||||
public static function head(array &$routes, string $route, callable $handler): void
|
||||
{
|
||||
self::add($routes, 'HEAD', $route, $handler);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sharkk\Router\SegmentRouter;
|
||||
|
||||
final class SegmentRouterTest extends TestCase
|
||||
{
|
||||
private SegmentRouter $router;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->router = new SegmentRouter();
|
||||
}
|
||||
|
||||
public function testAddAndLookupRoute(): void
|
||||
{
|
||||
$routes = [];
|
||||
SegmentRouter::add($routes, 'GET', '/blog/:id', function($id) {
|
||||
return "Blog post $id";
|
||||
});
|
||||
|
||||
$result = SegmentRouter::lookup($routes, 'GET', '/blog/123');
|
||||
$this->assertEquals(200, $result['code']);
|
||||
$this->assertIsCallable($result['handler']);
|
||||
$this->assertEquals(['123'], $result['params']);
|
||||
$this->assertEquals("Blog post 123", $result['handler'](...$result['params']));
|
||||
}
|
||||
|
||||
public function testLookupNotFound(): void
|
||||
{
|
||||
$routes = [];
|
||||
SegmentRouter::add($routes, 'GET', '/blog/:id', function($id) {
|
||||
return "Blog post $id";
|
||||
});
|
||||
|
||||
$result = SegmentRouter::lookup($routes, 'GET', '/nonexistent');
|
||||
$this->assertEquals(404, $result['code']);
|
||||
$this->assertNull($result['handler']);
|
||||
$this->assertEmpty($result['params']);
|
||||
}
|
||||
|
||||
public function testMethodNotAllowed(): void
|
||||
{
|
||||
$routes = [];
|
||||
SegmentRouter::add($routes, 'GET', '/blog/:id', function($id) {
|
||||
return "Blog post $id";
|
||||
});
|
||||
|
||||
$result = SegmentRouter::lookup($routes, 'POST', '/blog/123');
|
||||
$this->assertEquals(405, $result['code']);
|
||||
$this->assertNull($result['handler']);
|
||||
$this->assertEmpty($result['params']);
|
||||
}
|
||||
|
||||
public function testClearRoutes(): void
|
||||
{
|
||||
$routes = [];
|
||||
SegmentRouter::add($routes, 'GET', '/blog/:id', function($id) {
|
||||
return "Blog post $id";
|
||||
});
|
||||
|
||||
SegmentRouter::clear($routes);
|
||||
$this->assertEmpty($routes);
|
||||
}
|
||||
|
||||
public function testAddMultipleMethods(): void
|
||||
{
|
||||
$routes = [];
|
||||
SegmentRouter::add($routes, 'GET', '/blog/:id', function($id) {
|
||||
return "GET Blog post $id";
|
||||
});
|
||||
SegmentRouter::add($routes, 'POST', '/blog/:id', function($id) {
|
||||
return "POST Blog post $id";
|
||||
});
|
||||
|
||||
$resultGet = SegmentRouter::lookup($routes, 'GET', '/blog/123');
|
||||
$this->assertEquals(200, $resultGet['code']);
|
||||
$this->assertEquals("GET Blog post 123", $resultGet['handler'](...$resultGet['params']));
|
||||
|
||||
$resultPost = SegmentRouter::lookup($routes, 'POST', '/blog/123');
|
||||
$this->assertEquals(200, $resultPost['code']);
|
||||
$this->assertEquals("POST Blog post 123", $resultPost['handler'](...$resultPost['params']));
|
||||
}
|
||||
}
|
|
@ -8,28 +8,38 @@ $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'];
|
||||
|
||||
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)],
|
||||
};
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
|
||||
function makeRoute(): string
|
||||
{
|
||||
global $methods, $apis, $params, $endpoint, $midpoint;
|
||||
|
||||
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))));
|
||||
$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;
|
||||
}
|
||||
|
|
291
tests/test.php
291
tests/test.php
|
@ -2,8 +2,6 @@
|
|||
|
||||
require_once 'color.php';
|
||||
require_once __DIR__ . '/../src/RouterInterface.php';
|
||||
require_once __DIR__ . '/../src/SegmentRouterTrait.php';
|
||||
require_once __DIR__ . '/../src/SegmentRouterInterface.php';
|
||||
require_once __DIR__ . '/../src/SegmentRouter.php';
|
||||
|
||||
const ROUTES = __DIR__ . '/routes/';
|
||||
|
@ -13,189 +11,188 @@ use Sharkk\Router\SegmentRouter;
|
|||
|
||||
// if there's a flag, reset the opcache
|
||||
if (in_array('-f', $argv)) {
|
||||
opcache_reset();
|
||||
echoTitle("opcache reset");
|
||||
opcache_reset();
|
||||
echoTitle("opcache reset");
|
||||
}
|
||||
|
||||
// create the trees directory if it doesn't exist
|
||||
if (!is_dir(TREES)) mkdir(TREES);
|
||||
|
||||
$routes = [];
|
||||
$r = new SegmentRouter();
|
||||
|
||||
// Blog lookups
|
||||
$blog = readAndAddRoutes(ROUTES . 'blog.txt', $routes);
|
||||
writeRoutesToFile($routes, TREES . 'blog.txt');
|
||||
$blog = readAndAddRoutes(ROUTES . 'blog.txt', $r);
|
||||
writeRoutesToFile($r->dump(), TREES . 'blog.txt');
|
||||
echoTitle("Starting blog lookups");
|
||||
runIterations(10000, $routes, $blog);
|
||||
runIterations(100000, $routes, $blog);
|
||||
runIterations(1000000, $routes, $blog);
|
||||
runIterations(10000, $r, $blog);
|
||||
runIterations(100000, $r, $blog);
|
||||
runIterations(1000000, $r, $blog);
|
||||
unset($blog);
|
||||
|
||||
// Github lookups
|
||||
SegmentRouter::clear($routes);
|
||||
$github = readAndAddRoutes(ROUTES . 'github.txt', $routes);
|
||||
writeRoutesToFile($routes, TREES . 'github.txt');
|
||||
$r->clear();
|
||||
$github = readAndAddRoutes(ROUTES . 'github.txt', $r);
|
||||
writeRoutesToFile($r->dump(), TREES . 'github.txt');
|
||||
echoTitle("Starting github lookups");
|
||||
runIterations(10000, $routes, $github);
|
||||
runIterations(100000, $routes, $github);
|
||||
runIterations(1000000, $routes, $github);
|
||||
runIterations(10000, $r, $github);
|
||||
runIterations(100000, $r, $github);
|
||||
runIterations(1000000, $r, $github);
|
||||
unset($github);
|
||||
|
||||
// Big lookups
|
||||
SegmentRouter::clear($routes);
|
||||
$big = readAndAddRoutes(ROUTES . 'big.txt', $routes);
|
||||
writeRoutesToFile($routes, TREES . 'big.txt');
|
||||
$r->clear();
|
||||
$big = readAndAddRoutes(ROUTES . 'big.txt', $r);
|
||||
writeRoutesToFile($r->dump(), TREES . 'big.txt');
|
||||
echoTitle("Starting big lookups");
|
||||
runIterations(10000, $routes, $big);
|
||||
runIterations(100000, $routes, $big);
|
||||
runIterations(1000000, $routes, $big);
|
||||
runIterations(10000, $r, $big);
|
||||
runIterations(100000, $r, $big);
|
||||
runIterations(1000000, $r, $big);
|
||||
unset($big);
|
||||
|
||||
// Parameter testing
|
||||
SegmentRouter::clear($routes);
|
||||
$r->clear();
|
||||
echoTitle("Testing parameters");
|
||||
|
||||
$testRoutes = [
|
||||
['GET', '/blog/:id', function($id) {
|
||||
echo $id."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug', function($id, $slug) {
|
||||
echo $id . ' - ' . $slug."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug/:page', function($id, $slug, $page) {
|
||||
echo $id . ' - ' . $slug . ' - ' . $page."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug/:page/:extra', function($id, $slug, $page, $extra) {
|
||||
echo $id . ' - ' . $slug . ' - ' . $page . ' - ' . $extra."\n";
|
||||
}],
|
||||
$routes = [
|
||||
['GET', '/blog/:id', function($id) {
|
||||
echo $id."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug', function($id, $slug) {
|
||||
echo $id . ' - ' . $slug."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug/:page', function($id, $slug, $page) {
|
||||
echo $id . ' - ' . $slug . ' - ' . $page."\n";
|
||||
}],
|
||||
['GET', '/blog/:id/:slug/:page/:extra', function($id, $slug, $page, $extra) {
|
||||
echo $id . ' - ' . $slug . ' - ' . $page . ' - ' . $extra."\n";
|
||||
}],
|
||||
];
|
||||
|
||||
foreach ($testRoutes as $route) SegmentRouter::add($routes, ...$route);
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
[$method, $uri] = $testRoutes[array_rand($testRoutes)];
|
||||
|
||||
// Generate some random parameters
|
||||
$uri = str_replace(':id', rand(1, 100), $uri);
|
||||
$uri = str_replace(':slug', 'slug-' . rand(1, 100), $uri);
|
||||
$uri = str_replace(':page', rand(1, 100), $uri);
|
||||
$uri = str_replace(':extra', 'extra-' . rand(1, 100), $uri);
|
||||
|
||||
$res = SegmentRouter::lookup($routes, $method, $uri);
|
||||
if ($res['code'] !== 200) {
|
||||
echo "Failed to handle request for $uri - " . json_encode($res) . "\n";
|
||||
exit(1);
|
||||
}
|
||||
$res['handler'](...$res['params']);
|
||||
foreach ($routes as $route) {
|
||||
[$method, $path, $handler] = $route;
|
||||
$r->add($method, $path, $handler);
|
||||
}
|
||||
|
||||
SegmentRouter::clear($routes);
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
[$method, $uri] = $routes[array_rand($routes)];
|
||||
|
||||
// Generate some random parameters
|
||||
$uri = str_replace(':id', rand(1, 100), $uri);
|
||||
$uri = str_replace(':slug', 'slug-' . rand(1, 100), $uri);
|
||||
$uri = str_replace(':page', rand(1, 100), $uri);
|
||||
$uri = str_replace(':extra', 'extra-' . rand(1, 100), $uri);
|
||||
|
||||
$res = $r->lookup($method, $uri);
|
||||
if ($res['code'] !== 200) {
|
||||
echo "Failed to handle request for $uri - $res\n";
|
||||
exit(1);
|
||||
}
|
||||
$res['handler'](...$res['params']);
|
||||
}
|
||||
|
||||
$r->clear();
|
||||
echoTitle('Testing root node');
|
||||
|
||||
SegmentRouter::add($routes, 'GET', '/', function() {
|
||||
echo "Root node is gtg!\n";
|
||||
$r->add('GET', '/', function() {
|
||||
echo "Root node is gtg!\n";
|
||||
});
|
||||
|
||||
$res = SegmentRouter::lookup($routes, 'GET', '/');
|
||||
$res = $r->lookup('GET', '/');
|
||||
if ($res['code'] !== 200) {
|
||||
echo "Failed to handle request for /\n";
|
||||
exit(1);
|
||||
echo "Failed to handle request for /\n";
|
||||
exit(1);
|
||||
}
|
||||
$res['handler']();
|
||||
|
||||
echo "\n".Color::blue("✔️ Done!")."\n\n";
|
||||
|
||||
function echoTitle(string $title) {
|
||||
echo "\n";
|
||||
echo Color::bold(Color::black("===============================================================")) . "\n";
|
||||
echo "\n";
|
||||
echo Color::bold(Color::blue($title))."\n";
|
||||
echo "\n";
|
||||
echo Color::bold(Color::black("===============================================================")) . "\n";
|
||||
echo "\n";
|
||||
echo "\n";
|
||||
echo Color::bold(Color::black("===============================================================")) . "\n";
|
||||
echo "\n";
|
||||
echo Color::bold(Color::blue($title))."\n";
|
||||
echo "\n";
|
||||
echo Color::bold(Color::black("===============================================================")) . "\n";
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
function readAndAddRoutes(string $file, &$routes): array
|
||||
function readAndAddRoutes(string $file, &$r): array
|
||||
{
|
||||
return array_map(function($route) use (&$routes) {
|
||||
[$method, $path] = explode(' ', $route);
|
||||
$path = trim($path);
|
||||
SegmentRouter::add($routes, $method, $path, fn() => true);
|
||||
return [$method, $path];
|
||||
}, file($file));
|
||||
$array = [];
|
||||
$routes = file($file);
|
||||
foreach ($routes as $route) {
|
||||
[$method, $path] = explode(' ', $route);
|
||||
$path = trim($path);
|
||||
$array[] = [$method, $path];
|
||||
$r->add($method, $path, function() {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
function addRoutes($routes): array
|
||||
{
|
||||
return array_map(function($route) use (&$routes) {
|
||||
[$method, $path, $handler] = $route;
|
||||
SegmentRouter::add($routes, $method, $path, $handler);
|
||||
return [$method, $path, $handler];
|
||||
}, $routes);
|
||||
}
|
||||
function runIterations(int $iterations, $r, array $routes) {
|
||||
echo "\n🚀 Running $iterations iterations...\n";
|
||||
|
||||
function runIterations(int $iterations, &$routes, array $routesList) {
|
||||
echo "\n🚀 Running $iterations iterations...\n";
|
||||
$start = microtime(true); // start the timer
|
||||
$reqs = 0; // track the timing of lookups
|
||||
$longest = 0; // track the longest lookup time
|
||||
$shortest = 0; // track the shortest lookup time
|
||||
$longestRoute = '';
|
||||
$shortestRoute = '';
|
||||
|
||||
$start = microtime(true); // start the timer
|
||||
$reqs = 0; // track the timing of lookups
|
||||
$longest = 0; // track the longest lookup time
|
||||
$shortest = 0; // track the shortest lookup time
|
||||
$longestRoute = '';
|
||||
$shortestRoute = '';
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
// pick a random route from the array
|
||||
[$method, $path] = $routes[array_rand($routes)];
|
||||
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
// pick a random route from the array
|
||||
[$method, $path] = $routesList[array_rand($routesList)];
|
||||
// replace all :params/ with random values
|
||||
$uri = preg_replace_callback('/:(\w+)/', function($matches) {
|
||||
return rand(1, 100);
|
||||
}, $path);
|
||||
|
||||
// replace all :params/ with random values
|
||||
$uri = preg_replace_callback('/:(\w+)/', function($matches) {
|
||||
return rand(1, 100);
|
||||
}, $path);
|
||||
$start2 = microtime(true); // start the timer for the lookup
|
||||
|
||||
$start2 = microtime(true); // start the timer for the lookup
|
||||
$res = $r->lookup($method, $uri); // run the lookup
|
||||
|
||||
$res = SegmentRouter::lookup($routes, $method, $uri); // run the lookup
|
||||
$reqs += microtime(true) - $start2; // add this lookup time
|
||||
if ($shortest == 0 || microtime(true) - $start2 < $shortest) {
|
||||
$shortest = microtime(true) - $start2; // track the shortest lookup time
|
||||
$shortestRoute = "$method $uri";
|
||||
}
|
||||
if (microtime(true) - $start2 > $longest) {
|
||||
$longest = microtime(true) - $start2; // track the longest lookup time
|
||||
$longestRoute = "$method $uri";
|
||||
}
|
||||
|
||||
$reqs += microtime(true) - $start2; // add this lookup time
|
||||
if ($shortest == 0 || microtime(true) - $start2 < $shortest) {
|
||||
$shortest = microtime(true) - $start2; // track the shortest lookup time
|
||||
$shortestRoute = "$method $uri";
|
||||
}
|
||||
if (microtime(true) - $start2 > $longest) {
|
||||
$longest = microtime(true) - $start2; // track the longest lookup time
|
||||
$longestRoute = "$method $uri";
|
||||
}
|
||||
// if any error was encountered, print it and exit
|
||||
if ($res['code'] !== 200) {
|
||||
echo Color::red("Failed to handle request.\n$method {$res['code']}\n"."├─ URI: $uri\n└─ Path: $path\n");
|
||||
echo Color::yellow("Completed $i iterations before failure.\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
echo Color::blue("✔️ Done!")."\n";
|
||||
|
||||
// if any error was encountered, print it and exit
|
||||
if ($res['code'] !== 200) {
|
||||
echo Color::red("Failed to handle request.\n$method {$res['code']}\n"."├─ URI: $uri\n└─ Path: $path\n");
|
||||
echo Color::yellow("Completed $i iterations before failure.\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
echo Color::blue("✔️ Done!")."\n";
|
||||
// echo peak memory usage
|
||||
echo "Peak memory: " . Color::magenta(round(memory_get_peak_usage() / 1024, 1) . " kb\n");
|
||||
|
||||
// echo peak memory usage
|
||||
echo "Peak memory: " . Color::magenta(round(memory_get_peak_usage() / 1024, 1) . " kb\n");
|
||||
// total time used for this run
|
||||
echo "Time: " . Color::cyan(number_format(microtime(true) - $start, 10) . " s\n");
|
||||
|
||||
// total time used for this run
|
||||
echo "Time: " . Color::cyan(number_format(microtime(true) - $start, 10) . " s\n");
|
||||
// echo the average time per request
|
||||
echo "Avg/lookup: " . Color::yellow(number_format($reqs / $iterations, 10) . " s\n");
|
||||
|
||||
// echo the average time per request
|
||||
echo "Avg/lookup: " . Color::yellow(number_format($reqs / $iterations, 10) . " s\n");
|
||||
// echo the shortest lookup time
|
||||
echo "Shortest lookup: " . Color::green(number_format($shortest, 10) . " s\n");
|
||||
|
||||
// echo the shortest lookup time
|
||||
echo "Shortest lookup: " . Color::green(number_format($shortest, 10) . " s\n");
|
||||
// echo the longest lookup time
|
||||
echo "Longest lookup: " . Color::red(number_format($longest, 10) . " s\n");
|
||||
|
||||
// echo the longest lookup time
|
||||
echo "Longest lookup: " . Color::red(number_format($longest, 10) . " s\n");
|
||||
echo Color::black("==============================================") . "\n";
|
||||
|
||||
echo Color::black("==============================================") . "\n";
|
||||
|
||||
// echo the longest and shortest routes
|
||||
echo "Shortest route: " . Color::green($shortestRoute) . "\n";
|
||||
echo "Longest route: " . Color::red($longestRoute) . "\n";
|
||||
// echo the longest and shortest routes
|
||||
echo "Shortest route: " . Color::green($shortestRoute) . "\n";
|
||||
echo "Longest route: " . Color::red($longestRoute) . "\n";
|
||||
}
|
||||
|
||||
// take a route tree (array) and store it in a file to be read
|
||||
|
@ -205,8 +202,8 @@ function writeRoutesToFile(array $routes, string $file) {
|
|||
|
||||
$fp = fopen($file, 'w');
|
||||
|
||||
// write a / to the first line of the file
|
||||
fwrite($fp, "/\n");
|
||||
// write a / to the first line of the file
|
||||
fwrite($fp, "/\n");
|
||||
|
||||
// Start writing from the root level with an indentation of 0 and no prefix
|
||||
writeNode($routes, 0, '', $fp);
|
||||
|
@ -215,23 +212,23 @@ function writeRoutesToFile(array $routes, string $file) {
|
|||
}
|
||||
|
||||
function writeNode($node, $indent, $prefix, $fp) {
|
||||
$totalItems = count($node);
|
||||
$currentItem = 0;
|
||||
$totalItems = count($node);
|
||||
$currentItem = 0;
|
||||
|
||||
foreach ($node as $key => $value) {
|
||||
$currentItem++;
|
||||
$isLastChild = ($currentItem === $totalItems);
|
||||
$connector = $isLastChild ? '└── ' : '├── ';
|
||||
foreach ($node as $key => $value) {
|
||||
$currentItem++;
|
||||
$isLastChild = ($currentItem === $totalItems);
|
||||
$connector = $isLastChild ? '└── ' : '├── ';
|
||||
|
||||
$key = empty($key) ? '/' : $key;
|
||||
$key = empty($key) ? '/' : $key;
|
||||
|
||||
// Write the current node's key with the tree symbol
|
||||
fwrite($fp, $prefix . $connector . $key . "\n");
|
||||
// Write the current node's key with the tree symbol
|
||||
fwrite($fp, $prefix . $connector . $key . "\n");
|
||||
|
||||
// If the value is an array, it represents a child node, so recurse
|
||||
if (is_array($value)) {
|
||||
$newPrefix = $prefix . ($isLastChild ? ' ' : '│ ');
|
||||
writeNode($value, $indent + 1, $newPrefix, $fp);
|
||||
}
|
||||
}
|
||||
// If the value is an array, it represents a child node, so recurse
|
||||
if (is_array($value)) {
|
||||
$newPrefix = $prefix . ($isLastChild ? ' ' : '│ ');
|
||||
writeNode($value, $indent + 1, $newPrefix, $fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user