Compare commits

...

6 Commits

5 changed files with 66 additions and 39 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
tests/trees tests/trees
/vendor/ /vendor/
index.php

View File

@ -1,7 +1,7 @@
{ {
"name": "sharkk/router", "name": "sharkk/router",
"description": "A simple node-based router.", "description": "A simple node-based router.",
"version": "1.2.4", "version": "1.2.8",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
"autoload": { "autoload": {

View File

@ -2,7 +2,7 @@
namespace Sharkk\Router; namespace Sharkk\Router;
class SegmentRouter implements RouterInterface class Router
{ {
private array $routes = []; private array $routes = [];
@ -13,14 +13,14 @@ class SegmentRouter implements RouterInterface
* Example: * Example:
* `$r->add('GET', '/posts/:id', function($id) { echo "Viewing post $id"; });` * `$r->add('GET', '/posts/:id', function($id) { echo "Viewing post $id"; });`
*/ */
public function add(string $method, string $route, callable $handler): RouterInterface public function add(string $method, string $route, callable $handler): Router
{ {
// Expand the route into segments and make dynamic segments into a common placeholder // expand the route into segments and make dynamic segments into a common placeholder
$segments = array_map(function($segment) { $segments = array_map(function($segment) {
return str_starts_with($segment, ':') ? ':x' : $segment; return str_starts_with($segment, ':') ? ':x' : $segment;
}, explode('/', trim($route, '/'))); }, explode('/', trim($route, '/')));
// Push each segment into the routes array as a node, except if this is the root node // push each segment into the routes array as a node, except if this is the root node
$node = &$this->routes; $node = &$this->routes;
foreach ($segments as $segment) { foreach ($segments as $segment) {
// skip an empty segment, which allows us to register handlers for the root node // skip an empty segment, which allows us to register handlers for the root node
@ -43,48 +43,73 @@ class SegmentRouter implements RouterInterface
*/ */
public function lookup(string $method, string $uri): array 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 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 // init the response array
$params = []; $res = ['code' => 0, 'handler' => null, '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]) if (!$this->checkForHandlers($node)) {
? ['code' => 200, 'handler' => $node[$method], 'params' => null] $res['code'] = 404;
: ['code' => 405, 'handler' => null, 'params' => null]; return $res;
} }
// We'll split up the URI into segments and traverse the node tree 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) { foreach (explode('/', trim($uri, '/')) as $segment) {
// if there is a node for this segment, move to it // check that the next segment is an array (not a callable) and exists, then move to it
if (isset($node[$segment])) { if (isset($node[$segment]) && is_array($node[$segment])) {
$node = $node[$segment]; $node = $node[$segment];
continue; continue;
} }
// if there is a dynamic segment, move to it and store the value // if there is a dynamic segment, move to it and store the value
if (isset($node[':x'])) { if (isset($node[':x'])) {
$params[] = $segment; $res['params'][] = $segment;
$node = $node[':x']; $node = $node[':x'];
continue; continue;
} }
// if we can't find a node for this segment, return 404 // if we can't find a node for this segment, return 404
return ['code' => 404, 'handler' => null, 'params' => []]; $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 we found a handler for the method, return it and any params. if not, return a 405
return isset($node[$method]) if (isset($node[$method])) {
? ['code' => 200, 'handler' => $node[$method], 'params' => $params ?? []] $res['code'] = 200;
: ['code' => 405, 'handler' => null, 'params' => []]; $res['handler'] = $node[$method];
} else {
$res['code'] = 405;
}
return $res;
} }
/** /**
* Clear all routes from the router. * Clear all routes from the router.
*/ */
public function clear(): RouterInterface public function clear(): Router
{ {
$this->routes = []; $this->routes = [];
return $this; return $this;
@ -98,10 +123,22 @@ class SegmentRouter implements RouterInterface
return $this->routes; 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. * Register a GET route.
*/ */
public function get(string $route, callable $handler): RouterInterface public function get(string $route, callable $handler): Router
{ {
return $this->add('GET', $route, $handler); return $this->add('GET', $route, $handler);
} }
@ -109,7 +146,7 @@ class SegmentRouter implements RouterInterface
/** /**
* Register a POST route. * Register a POST route.
*/ */
public function post(string $route, callable $handler): RouterInterface public function post(string $route, callable $handler): Router
{ {
return $this->add('POST', $route, $handler); return $this->add('POST', $route, $handler);
} }
@ -117,7 +154,7 @@ class SegmentRouter implements RouterInterface
/** /**
* Register a PUT route. * Register a PUT route.
*/ */
public function put(string $route, callable $handler): RouterInterface public function put(string $route, callable $handler): Router
{ {
return $this->add('PUT', $route, $handler); return $this->add('PUT', $route, $handler);
} }
@ -125,7 +162,7 @@ class SegmentRouter implements RouterInterface
/** /**
* Register a PATCH route. * Register a PATCH route.
*/ */
public function patch(string $route, callable $handler): RouterInterface public function patch(string $route, callable $handler): Router
{ {
return $this->add('PATCH', $route, $handler); return $this->add('PATCH', $route, $handler);
} }
@ -133,7 +170,7 @@ class SegmentRouter implements RouterInterface
/** /**
* Register a DELETE route. * Register a DELETE route.
*/ */
public function delete(string $route, callable $handler): RouterInterface public function delete(string $route, callable $handler): Router
{ {
return $this->add('DELETE', $route, $handler); return $this->add('DELETE', $route, $handler);
} }
@ -141,7 +178,7 @@ class SegmentRouter implements RouterInterface
/** /**
* Register a HEAD route. * Register a HEAD route.
*/ */
public function head(string $route, callable $handler): RouterInterface public function head(string $route, callable $handler): Router
{ {
return $this->add('HEAD', $route, $handler); return $this->add('HEAD', $route, $handler);
} }

View File

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

View File

@ -1,13 +1,12 @@
<?php <?php
require_once 'color.php'; require_once 'color.php';
require_once __DIR__ . '/../src/RouterInterface.php'; require_once __DIR__ . '/../src/Router.php';
require_once __DIR__ . '/../src/SegmentRouter.php';
const ROUTES = __DIR__ . '/routes/'; const ROUTES = __DIR__ . '/routes/';
const TREES = __DIR__ . '/trees/'; const TREES = __DIR__ . '/trees/';
use Sharkk\Router\SegmentRouter; use Sharkk\Router\Router;
// if there's a flag, reset the opcache // if there's a flag, reset the opcache
if (in_array('-f', $argv)) { if (in_array('-f', $argv)) {
@ -18,7 +17,7 @@ if (in_array('-f', $argv)) {
// create the trees directory if it doesn't exist // create the trees directory if it doesn't exist
if (!is_dir(TREES)) mkdir(TREES); if (!is_dir(TREES)) mkdir(TREES);
$r = new SegmentRouter(); $r = new Router();
// Blog lookups // Blog lookups
$blog = readAndAddRoutes(ROUTES . 'blog.txt', $r); $blog = readAndAddRoutes(ROUTES . 'blog.txt', $r);