Condense the Router to just the SegmentRouter, and format as a Composer package

This commit is contained in:
Sky Johnson 2024-09-12 09:28:53 -05:00
parent 9e35166eea
commit 55306b27b9
24 changed files with 157 additions and 20125 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
tests/trees
/vendor/

View File

@ -1,6 +0,0 @@
<?php
interface Router {
public function add(string $method, string $route, callable $handler): Router;
public function lookup(string $method, string $uri): array;
}

View File

@ -1,254 +0,0 @@
<?php
class SimpleRouter
{
private static array $routes = [];
private static $pathNotFound;
private static $methodNotAllowed;
private static string $defaultConstraint = '([\w\-]+)';
private static string $currentPrefix = '';
private static string $lastInsertedRoute = '';
/**
* A quick static function to register a route in the router. Used by the shorthand methods as well.
*
* @param string $route The path to be used as the route.
* @param callable|string $action Either a callable to be executed, or a string reference to a method.
* @param string|array $methods The HTTP verb(s) this route accepts.
* @return Router
*/
public static function add(string $route, callable|string $action, string|array $methods = 'GET')
{
// If a prefix exists, prepend it to the route
if (!empty(self::$currentPrefix)) {
$route = self::$currentPrefix.$route;
}
$trimmed = self::trimRoute($route);
self::$routes[] = [
'route' => $trimmed,
'action' => $action,
'methods' => $methods,
'constraints' => []
];
self::$lastInsertedRoute = $trimmed;
return new self;
}
/**
* Shorthand function to define a GET route
*
* @param string $route
* @param callable $action
* @return Router
*/
public static function get(string $route, callable $action)
{
return self::add($route, $action, 'GET');
}
/**
* Default function to define a POST route
*
* @param string $route
* @param callable $action
* @return Router
*/
public static function post(string $route, callable $action)
{
return self::add($route, $action, 'POST');
}
/**
* Return all routes currently registered
*
* @return array
*/
public static function getAllRoutes()
{
return self::$routes;
}
/**
* Defines an action to be called when a path isn't found - i.e. a 404
*
* @param callable $action
* @return void
*/
public static function pathNotFound(callable $action)
{
self::$pathNotFound = $action;
}
/**
* Defines an action to be called with a method isn't allowed on a route - i.e. a 405
*
* @param callable $action
* @return void
*/
public static function methodNotAllowed(callable $action)
{
self::$methodNotAllowed = $action;
}
/**
* Redefine the default constraint for route parameters. Default is '([\w\-]+)'
*
* @param string $constraint The RegEx you want parameters to adhere to by default. Defaults to '([\w\-]+)'
* @return void
*/
public static function setDefaultConstraint(string $constraint = '([\w\-]+)')
{
self::$defaultConstraint = $constraint;
}
private static function trimRoute(string $route): string
{
$route = trim(trim($route), '/');
return "/$route";
}
/**
* Accepts a callable that defines routes, and adds a prefix to them.
*
* @param string $prefix The prefix you want added to the routes.
* @param callable $routes A function that defines routes.
* @return void
*/
public static function prefix(string $prefix, callable $routes)
{
self::$currentPrefix = $prefix;
$routes();
self::$currentPrefix = '';
}
/**
* Define a constraint for a route parameter. If only passing one parameter,
* provide the parameter name as first argument and constraint as second. If
* adding constraints for multiple parameters, pass an array of 'parameter' => 'constraint'
* pairs.
*
* @param string|array $parameter
* @param string $constraint
* @return Router
*/
public static function with(string|array $parameter, string $constraint = '')
{
$last = self::$lastInsertedRoute;
if (is_array($parameter)) {
foreach ($parameter as $param => $constraint) {
self::$routes[$last]['constraints'][$param] = $constraint;
}
return new self;
}
self::$routes[$last]['constraints'][$parameter] = $constraint;
return new self;
}
/**
* Tokenizes the given URI using our constraint rules and returns the tokenized URI
*
* @param string $uri
* @return string
*/
private static function tokenize(string $uri, array $constraints)
{
$constraintKeys = array_keys($constraints);
preg_match_all('/(?:{([\w\-]+)})+/', $uri, $matches);
$matches = $matches[1];
foreach ($matches as $match) {
$pattern = '{'.$match.'}';
if (in_array($match, $constraintKeys)) {
// Do some voodoo to allow users to use parentheses in their constraints if they want
$constraint = '('.rtrim(ltrim(trim($constraints[$match]), '('), ')').')';
$uri = str_replace($pattern, $constraint, $uri);
} else {
$uri = str_replace($pattern, self::$defaultConstraint, $uri);
}
}
return $uri;
}
/**
* Runs the router. Accepts a base path from which to serve the routes, and optionally whether you want to try
* and match multiple routes.
*
* @param string $basePath
* @param boolean $multimatch
* @return void
*/
public static function run(string $uri, string $basePath = '', bool $multimatch = false, string $method = ''): int|array
{
$basePath = self::trimRoute($basePath);
$path = urldecode(self::trimRoute($uri));
$pathMatchFound = false;
$routeMatchFound = false;
// Begin looking through routes
foreach (self::$routes as $route) {
// If the basePath isn't just "root"
if ($basePath != '/') {
$route['route'] = self::trimRoute($basePath.$route['route']);
}
// Prepare route by tokenizing.
$tokenized = '#^'.self::tokenize($route['route'], $route['constraints']).'$#u';
// If the tokenized route matches the current path...
if (preg_match($tokenized, $path, $matches)) {
$pathMatchFound = true;
// Run through the route's accepted method(s)
foreach ((array) $route['methods'] as $allowedMethod) {
// See if the current request method matches
if (strtolower($method) == strtolower($allowedMethod)) {
array_shift($matches); // Remove the first match - always contains the full url
// If we're successful at calling the route's action, echo the result
return [$route['action'], $matches];
$routeMatchFound = true;
// Do not check other routes.
break;
}
}
}
// Break the loop if the first found route is a match.
if($routeMatchFound && !$multimatch) {
break;
}
}
// No matching route was found
if (!$routeMatchFound) {
// But a matching path exists
if ($pathMatchFound) {
return 405;
} else {
return 404;
}
}
}
public static function clearRoutes()
{
self::$routes = [];
}
}

View File

@ -1,25 +0,0 @@
<?php
class StaticRouter
{
public static function lookup(array $node, string $method, string $uri): int|array
{
$uriSegments = explode('/', trim($uri, '/'));
$params = [];
if (isset($node[$method])) return [$node[$method], $params];
if (! $node2 = array_reduce($uriSegments, function ($carry, $segment) use (&$params) {
if (isset($carry[$segment])) return $carry[$segment];
if (isset($carry[':x'])) {
$params[] = $segment;
return $carry[':x'];
}
return null;
}, $node)) return 404;
if (isset($node2[$method])) return [$node2[$method], $params];
return 405;
}
}

View File

@ -1,70 +0,0 @@
<?php
class TrieRouter implements Router
{
public array $root = [];
// Add route to the trie
public function add(string $method, string $route, callable $handler): Router
{
$node = &$this->root[$method];
// 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, '/')));
foreach ($segments as $segment) {
if (!isset($node[$segment])) {
$node[$segment] = ['_children' => [], '_handler' => null];
}
$node = &$node[$segment]['_children'];
}
$node['_handler'] = $handler;
return $this;
}
// Find and handle the route
public function lookup(string $method, string $uri): array
{
$node = &$this->root[$method];
$segments = explode('/', trim($uri, '/'));
$params = [];
foreach ($segments as $segment) {
if (isset($node[$segment])) {
$node = &$node[$segment]['_children'];
} else {
// Handle dynamic parameters (e.g., :id)
$dynamicSegment = $this->matchDynamicSegment($node, $segment);
if ($dynamicSegment) {
$params[] = $segment;
$node = &$node[$dynamicSegment]['_children'];
} else {
return ['code' => 404];
}
}
}
// Check if a handler exists for the current node
if (isset($node['_handler'])) return ['code' => 200, 'handler' => $node['_handler'], 'params' => $params];
return ['code' => 404];
}
// Match dynamic route segments like ':id'
private function matchDynamicSegment(array $node, string $segment)
{
foreach ($node as $key => $value) {
if (strpos($key, ':') === 0) return $key;
}
return null;
}
public function clear(): Router
{
$this->root = [];
return $this;
}
}

19
composer.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "sharkk/router",
"description": "A simple node-based router.",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"Sharkk\\Router\\": "src/"
}
},
"authors": [
{
"name": "Sharkk",
"email": "email@sharkk.net"
}
],
"minimum-stability": "stable",
"require": {}
}

View File

View File

@ -1,6 +1,8 @@
<?php
class SegmentRouter implements Router
namespace Sharkk\Router;
class Router
{
public array $routes = [];

64
tests/color.php Normal file
View File

@ -0,0 +1,64 @@
<?php
class Color {
const RESET = "\033[0m";
const BOLD = "\033[1m";
const UNDERLINE = "\033[4m";
const INVERSE = "\033[7m";
const BLACK = "\033[30m";
const RED = "\033[31m";
const GREEN = "\033[32m";
const YELLOW = "\033[33m";
const BLUE = "\033[34m";
const MAGENTA = "\033[35m";
const CYAN = "\033[36m";
const WHITE = "\033[37m";
private static function format(string $color, string $string): string {
return $color . $string . self::RESET;
}
public static function bold(string $string): string {
return self::format(self::BOLD, $string);
}
public static function underline(string $string): string {
return self::format(self::UNDERLINE, $string);
}
public static function inverse(string $string): string {
return self::format(self::INVERSE, $string);
}
public static function black(string $string): string {
return self::format(self::BLACK, $string);
}
public static function red(string $string): string {
return self::format(self::RED, $string);
}
public static function green(string $string): string {
return self::format(self::GREEN, $string);
}
public static function yellow(string $string): string {
return self::format(self::YELLOW, $string);
}
public static function blue(string $string): string {
return self::format(self::BLUE, $string);
}
public static function magenta(string $string): string {
return self::format(self::MAGENTA, $string);
}
public static function cyan(string $string): string {
return self::format(self::CYAN, $string);
}
public static function white(string $string): string {
return self::format(self::WHITE, $string);
}
}

View File

@ -12,7 +12,7 @@ $midpoint = ['do', 'cause', 'effect', 'affect', 'impact', 'influence', 'change',
for ($i = 0; $i < 1000; $i++) {
$routes[] = makeRoute();
// write the routes array to a file
file_put_contents('big.txt', implode("\n", $routes));
file_put_contents('routes/big.txt', implode("\n", $routes));
}
function makeRoute(): string

View File

@ -1,83 +0,0 @@
<?php
/*
This test file puts the SegmentRouter to the test by running a million lookups on a handful
of routes. The routes are read from two files, blog.txt and github.txt, and added to two separate
routers. The lookups are then run in two separate loops, one for each router.
Each lookup is timed and the memory usage is also printed out at regular intervals.
The requests are randomly picked from the array of routes.
*/
require_once 'tools.php';
$r = new SegmentRouter();
// Blog lookups
$blog = readAndAddRoutes('blog.txt', $r);
writeRoutesToFile($r->routes, 'storage/segment/blog.txt');
echoTitle("Starting blog lookups");
runIterations(10000, $r, $blog);
runIterations(100000, $r, $blog);
runIterations(1000000, $r, $blog);
unset($blog);
// Github lookups
$r->clear();
$github = readAndAddRoutes('github.txt', $r);
writeRoutesToFile($r->routes, 'storage/segment/github.txt');
echoTitle("Starting github lookups");
runIterations(10000, $r, $github);
runIterations(100000, $r, $github);
runIterations(1000000, $r, $github);
unset($github);
// Big lookups
$r->clear();
$big = readAndAddRoutes('big.txt', $r);
writeRoutesToFile($r->routes, 'storage/segment/big.txt');
echoTitle("Starting big lookups");
runIterations(10000, $r, $big);
runIterations(100000, $r, $big);
runIterations(1000000, $r, $big);
unset($big);
// Parameter testing
$r->clear();
echoTitle("Testing parameters");
$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 ($routes as $route) {
[$method, $path, $handler] = $route;
$r->add($method, $path, $handler);
}
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']);
}

View File

@ -1,77 +0,0 @@
<?php
/*
This test file puts the SimpleRouter to the test by running a million lookups on a handful
of routes. The routes are read from two files, blog.txt and github.txt, and added to two separate
routers and two separate arrays. The lookups are then run in two separate loops, one for each router.
Each lookup is timed and the memory usage is also printed out at regular intervals.
The requests are randomly picked from the array of routes.
*/
require_once 'tools.php';
require_once __DIR__ . '/../SimpleRouter.php';
// Blog test
$blog = sReadAndAddRoutes('blog.txt');
echoTitle("Starting blog lookups");
sRunIterations(100000, $blog);
sRunIterations(1000000, $blog);
// Github test
SimpleRouter::clearRoutes();
$github = sReadAndAddRoutes('github.txt');
echoTitle("Starting github lookups");
sRunIterations(10000, $github);
sRunIterations(100000, $github);
sRunIterations(1000000, $github);
// Big test; since simplerouter is so much slower, we'll only run the big test if the -b flag is passed
if (in_array('-b', $argv)) {
SimpleRouter::clearRoutes();
$big = sReadAndAddRoutes('big.txt');
echoTitle("Starting big lookups");
sRunIterations(10000, $big);
sRunIterations(100000, $big);
sRunIterations(1000000, $big);
}
function sReadAndAddRoutes(string $file): array
{
$array = [];
$routes = file($file);
foreach ($routes as $route) {
[$method, $path] = explode(' ', $route);
$path = trim($path);
// convert params from :param to {param}
$path = preg_replace('/:(\w+)/', '{$1}', $path);
$array[] = [$method, $path];
SimpleRouter::add($path, function() {
return true;
}, $method);
}
return $array;
}
function sRunIterations(int $iterations, array $routes) {
echo "Running $iterations iterations\n";
$start = microtime(true);
$interval = $iterations / 10;
for ($i = 0; $i < $iterations; $i++) {
// pick a random route from the array
[$method, $uri] = $routes[array_rand($routes)];
// replace the params with random values
$uri = preg_replace_callback('/{(\w+)}/', function($matches) {
return $matches[1] . '-' . rand(1, 100);
}, $uri);
$res = SimpleRouter::run($uri, '', false, $method);
if ($res === 404 || $res === 405) {
echo Color::red("Failed to handle request for $uri - $res\n");
exit(1);
}
if ($i !== 0 && $i % ($interval) === 0) echoMemoryAndTime($i, $start);
}
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((microtime(true) - $start) / $iterations, 10) . " s\n");
echo "\n";
}

View File

@ -1,44 +0,0 @@
<?php
require_once 'tools.php';
require_once __DIR__ . '/../StaticRouter.php';
$r = new SegmentRouter();
// Big test
$big = readRoutes('big.txt');
foreach ($big as $route) {
[$method, $path] = $route;
$r->add($method, $path, function() {
return true;
});
}
echoTitle("Starting big lookups");
$start = microtime(true);
$reqs = 0;
for ($i = 0; $i < 1000000; $i++) {
$index = array_rand($big);
[$method, $path] = $big[$index];
$rstart = microtime(true);
$res = StaticRouter::lookup($r->routes, $method, $path);
if ($res === 404 || $res === 405) {
die("404 or 405\n");
}
$reqs += microtime(true) - $rstart;
}
echo "Time: " . Color::cyan(number_format(microtime(true) - $start, 10) . " s\n");
echo "Peak memory: " . Color::magenta(round(memory_get_peak_usage() / 1024, 1) . " kb\n");
echo "Avg/lookup: " . Color::yellow(number_format($reqs / 1000000, 10) . " s\n");
function readRoutes(string $file): array
{
$array = [];
$routes = file($file);
foreach ($routes as $route) {
[$method, $path] = explode(' ', $route);
$path = trim($path);
$array[] = [$method, $path];
}
return $array;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
/
├── GET
├── :x
│ └── GET
├── tags
│ └── GET
└── tag
└── :x
└── GET

View File

@ -1,376 +0,0 @@
/
├── authorizations
│ ├── GET
│ ├── :x
│ │ ├── GET
│ │ └── DELETE
│ └── POST
├── applications
│ └── :x
│ └── tokens
│ ├── :x
│ │ ├── GET
│ │ └── DELETE
│ └── DELETE
├── events
│ └── GET
├── repos
│ └── :x
│ └── :x
│ ├── events
│ │ └── GET
│ ├── notifications
│ │ ├── GET
│ │ └── PUT
│ ├── stargazers
│ │ └── GET
│ ├── subscribers
│ │ └── GET
│ ├── subscription
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ ├── git
│ │ ├── blobs
│ │ │ ├── :x
│ │ │ │ └── GET
│ │ │ └── POST
│ │ ├── commits
│ │ │ ├── :x
│ │ │ │ └── GET
│ │ │ └── POST
│ │ ├── refs
│ │ │ ├── GET
│ │ │ └── POST
│ │ ├── tags
│ │ │ ├── :x
│ │ │ │ └── GET
│ │ │ └── POST
│ │ └── trees
│ │ ├── :x
│ │ │ └── GET
│ │ └── POST
│ ├── issues
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ ├── comments
│ │ │ │ ├── GET
│ │ │ │ └── POST
│ │ │ ├── events
│ │ │ │ └── GET
│ │ │ └── labels
│ │ │ ├── GET
│ │ │ ├── POST
│ │ │ ├── :x
│ │ │ │ └── DELETE
│ │ │ ├── PUT
│ │ │ └── DELETE
│ │ └── POST
│ ├── assignees
│ │ ├── GET
│ │ └── :x
│ │ └── GET
│ ├── labels
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ └── DELETE
│ │ └── POST
│ ├── milestones
│ │ ├── :x
│ │ │ ├── labels
│ │ │ │ └── GET
│ │ │ ├── GET
│ │ │ └── DELETE
│ │ ├── GET
│ │ └── POST
│ ├── pulls
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ ├── commits
│ │ │ │ └── GET
│ │ │ ├── files
│ │ │ │ └── GET
│ │ │ ├── merge
│ │ │ │ ├── GET
│ │ │ │ └── PUT
│ │ │ └── comments
│ │ │ ├── GET
│ │ │ └── PUT
│ │ └── POST
│ ├── GET
│ ├── contributors
│ │ └── GET
│ ├── languages
│ │ └── GET
│ ├── teams
│ │ └── GET
│ ├── tags
│ │ └── GET
│ ├── branches
│ │ ├── GET
│ │ └── :x
│ │ └── GET
│ ├── DELETE
│ ├── collaborators
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ ├── comments
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ └── DELETE
│ ├── commits
│ │ ├── :x
│ │ │ ├── comments
│ │ │ │ ├── GET
│ │ │ │ └── POST
│ │ │ └── GET
│ │ └── GET
│ ├── readme
│ │ └── GET
│ ├── keys
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ └── DELETE
│ │ └── POST
│ ├── downloads
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ └── DELETE
│ ├── forks
│ │ ├── GET
│ │ └── POST
│ ├── hooks
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ ├── tests
│ │ │ │ └── POST
│ │ │ └── DELETE
│ │ └── POST
│ ├── merges
│ │ └── POST
│ ├── releases
│ │ ├── GET
│ │ ├── :x
│ │ │ ├── GET
│ │ │ ├── DELETE
│ │ │ └── assets
│ │ │ └── GET
│ │ └── POST
│ ├── stats
│ │ ├── contributors
│ │ │ └── GET
│ │ ├── commit_activity
│ │ │ └── GET
│ │ ├── code_frequency
│ │ │ └── GET
│ │ ├── participation
│ │ │ └── GET
│ │ └── punch_card
│ │ └── GET
│ └── statuses
│ └── :x
│ ├── GET
│ └── POST
├── networks
│ └── :x
│ └── :x
│ └── events
│ └── GET
├── orgs
│ └── :x
│ ├── events
│ │ └── GET
│ ├── issues
│ │ └── GET
│ ├── GET
│ ├── members
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ └── DELETE
│ ├── public_members
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ ├── teams
│ │ ├── GET
│ │ └── POST
│ └── repos
│ ├── GET
│ └── POST
├── users
│ ├── :x
│ │ ├── received_events
│ │ │ ├── GET
│ │ │ └── public
│ │ │ └── GET
│ │ ├── events
│ │ │ ├── GET
│ │ │ ├── public
│ │ │ │ └── GET
│ │ │ └── orgs
│ │ │ └── :x
│ │ │ └── GET
│ │ ├── starred
│ │ │ └── GET
│ │ ├── subscriptions
│ │ │ └── GET
│ │ ├── gists
│ │ │ └── GET
│ │ ├── orgs
│ │ │ └── GET
│ │ ├── repos
│ │ │ └── GET
│ │ ├── GET
│ │ ├── followers
│ │ │ └── GET
│ │ ├── following
│ │ │ ├── GET
│ │ │ └── :x
│ │ │ └── GET
│ │ └── keys
│ │ └── GET
│ └── GET
├── feeds
│ └── GET
├── notifications
│ ├── GET
│ ├── PUT
│ └── threads
│ └── :x
│ ├── GET
│ └── subscription
│ ├── GET
│ ├── PUT
│ └── DELETE
├── user
│ ├── starred
│ │ ├── GET
│ │ └── :x
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ ├── subscriptions
│ │ ├── GET
│ │ └── :x
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ ├── issues
│ │ └── GET
│ ├── orgs
│ │ └── GET
│ ├── teams
│ │ └── GET
│ ├── repos
│ │ ├── GET
│ │ └── POST
│ ├── GET
│ ├── emails
│ │ ├── GET
│ │ ├── POST
│ │ └── DELETE
│ ├── followers
│ │ └── GET
│ ├── following
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ └── keys
│ ├── GET
│ ├── :x
│ │ ├── GET
│ │ └── DELETE
│ └── POST
├── gists
│ ├── GET
│ ├── :x
│ │ ├── GET
│ │ ├── star
│ │ │ ├── PUT
│ │ │ ├── DELETE
│ │ │ └── GET
│ │ ├── forks
│ │ │ └── POST
│ │ └── DELETE
│ └── POST
├── issues
│ └── GET
├── emojis
│ └── GET
├── gitignore
│ └── templates
│ ├── GET
│ └── :x
│ └── GET
├── markdown
│ ├── POST
│ └── raw
│ └── POST
├── meta
│ └── GET
├── rate_limit
│ └── GET
├── teams
│ └── :x
│ ├── GET
│ ├── DELETE
│ ├── members
│ │ ├── GET
│ │ └── :x
│ │ ├── GET
│ │ ├── PUT
│ │ └── DELETE
│ └── repos
│ ├── GET
│ └── :x
│ └── :x
│ ├── GET
│ ├── PUT
│ └── DELETE
├── repositories
│ └── GET
├── search
│ ├── repositories
│ │ └── GET
│ ├── code
│ │ └── GET
│ ├── issues
│ │ └── GET
│ └── users
│ └── GET
└── legacy
├── issues
│ └── search
│ └── :x
│ └── :x
│ └── :x
│ └── :x
│ └── GET
├── repos
│ └── search
│ └── :x
│ └── GET
└── user
├── search
│ └── :x
│ └── GET
└── email
└── :x
└── GET

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
/
└── GET
├── /
│ ├── _children
│ │ └── _handler
│ └── _handler
├── :x
│ ├── _children
│ │ └── _handler
│ └── _handler
├── tags
│ ├── _children
│ │ └── _handler
│ └── _handler
└── tag
├── _children
│ └── :x
│ ├── _children
│ │ └── _handler
│ └── _handler
└── _handler

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +1,86 @@
<?php
require_once 'color.php';
require_once __DIR__ . '/../src/Router.php';
use Sharkk\Router\Router;
// if there's a flag, reset the opcache
if (in_array('-f', $argv)) {
opcache_reset();
echoTitle("opcache reset");
}
require_once __DIR__ . '/../Router.php';
require_once __DIR__ . '/../SegmentRouter.php';
require_once __DIR__ . '/../TrieRouter.php';
$r = new Router();
// A simple class to return a string wrapped in ANSI color codes for terminal output
class Color {
const RESET = "\033[0m";
const BOLD = "\033[1m";
const UNDERLINE = "\033[4m";
const INVERSE = "\033[7m";
const BLACK = "\033[30m";
const RED = "\033[31m";
const GREEN = "\033[32m";
const YELLOW = "\033[33m";
const BLUE = "\033[34m";
const MAGENTA = "\033[35m";
const CYAN = "\033[36m";
const WHITE = "\033[37m";
// Blog lookups
$blog = readAndAddRoutes('routes/blog.txt', $r);
writeRoutesToFile($r->routes, 'trees/blog.txt');
echoTitle("Starting blog lookups");
runIterations(10000, $r, $blog);
runIterations(100000, $r, $blog);
runIterations(1000000, $r, $blog);
unset($blog);
private static function format(string $color, string $string): string {
return $color . $string . self::RESET;
}
// Github lookups
$r->clear();
$github = readAndAddRoutes('routes/github.txt', $r);
writeRoutesToFile($r->routes, 'trees/github.txt');
echoTitle("Starting github lookups");
runIterations(10000, $r, $github);
runIterations(100000, $r, $github);
runIterations(1000000, $r, $github);
unset($github);
public static function bold(string $string): string {
return self::format(self::BOLD, $string);
}
// Big lookups
$r->clear();
$big = readAndAddRoutes('routes/big.txt', $r);
writeRoutesToFile($r->routes, 'trees/big.txt');
echoTitle("Starting big lookups");
runIterations(10000, $r, $big);
runIterations(100000, $r, $big);
runIterations(1000000, $r, $big);
unset($big);
public static function underline(string $string): string {
return self::format(self::UNDERLINE, $string);
}
// Parameter testing
$r->clear();
echoTitle("Testing parameters");
public static function inverse(string $string): string {
return self::format(self::INVERSE, $string);
}
$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";
}],
];
public static function black(string $string): string {
return self::format(self::BLACK, $string);
}
public static function red(string $string): string {
return self::format(self::RED, $string);
}
public static function green(string $string): string {
return self::format(self::GREEN, $string);
}
public static function yellow(string $string): string {
return self::format(self::YELLOW, $string);
}
public static function blue(string $string): string {
return self::format(self::BLUE, $string);
}
public static function magenta(string $string): string {
return self::format(self::MAGENTA, $string);
}
public static function cyan(string $string): string {
return self::format(self::CYAN, $string);
}
public static function white(string $string): string {
return self::format(self::WHITE, $string);
}
foreach ($routes as $route) {
[$method, $path, $handler] = $route;
$r->add($method, $path, $handler);
}
function echoMemoryAndTime(int $i, float $start) {
echo "(".Color::green("$i lookups").") M: " .
Color::blue(round(memory_get_usage() / 1024, 1) . " kb") .
" - T: ". Color::blue(number_format(microtime(true) - $start, 10) ." s") .
"\n";
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']);
}
function echoTitle(string $title) {
@ -207,4 +209,3 @@ function writeNode($node, $indent, $prefix, $fp) {
}
}
}

View File

@ -1,38 +0,0 @@
<?php
/*
This test file puts the PATRICIA trie to the test by running a million lookups on a handful
of routes. The routes are read from two files, blog.txt and github.txt, and added to two separate
routers and two separate arrays. The lookups are then run in two separate loops, one for each router.
Each lookup is timed and the memory usage is also printed out at regular intervals.
The requests are randomly picked from the array of routes.
*/
require_once 'tools.php';
$r = new TrieRouter();
// Blog test
$blog = readAndAddRoutes('blog.txt', $r);
writeRoutesToFile($r->root, 'storage/trie/blog.txt');
echoTitle("Starting blog lookups");
runIterations(100000, $r, $blog);
runIterations(1000000, $r, $blog);
// Github test
$r->clear();
$github = readAndAddRoutes('github.txt', $r);
writeRoutesToFile($r->root, 'storage/trie/github.txt');
echoTitle("Starting github lookups");
runIterations(10000, $r, $github);
runIterations(100000, $r, $github);
runIterations(1000000, $r, $github);
// Big test
$r->clear();
$big = readAndAddRoutes('big.txt', $r);
writeRoutesToFile($r->root, 'storage/trie/big.txt');
echoTitle("Starting big lookups");
runIterations(10000, $r, $big);
runIterations(100000, $r, $big);
runIterations(1000000, $r, $big);