dev/src/Router.php

186 lines
4.4 KiB
PHP

<?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);
}
}