Addin a lot of changes

This commit is contained in:
2026-08-20 00:20:17 -03:00
parent 03e9f93e00
commit 795062bb6d
5 changed files with 1082 additions and 468 deletions
+459 -263
View File
@@ -2,35 +2,48 @@
namespace Routes;
use stdClass;
use Exception;
class RouteCollection
{
private static $_routeCollection;
private static RouteCollection $_routeCollection;
public $_uri = '/';
public $_routes = array();
private $_verb = '';
private $_loadedFiles = array();
private $_errors = array();
private $_defaultMiddlewares = array();
private $_middlewareSet = array();
private static $_verbsWhitelist = array('*', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'CLI');
private static $_verbsWeb = array('GET', 'POST', 'PUT', 'PATCH', 'DELETE');
public $_groupIn = false;
public $_groupBase = '';
public $_groupList = [];
public $_groups = [];
public string $_uri = '/';
public array $_routes = array();
private string $_verb = '';
private array $_loadedFiles = array();
private array $_errors = array();
private array $_defaultMiddlewares = array();
private array $_middlewareSet = array();
private static array $_verbsWhitelist = array('*', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'CLI');
private static array $_verbsWeb = array('GET', 'POST', 'PUT', 'PATCH', 'DELETE');
public bool $_groupIn = false;
public string $_groupBase = '';
public array $_groupList = array();
public array $_groups = array();
public array $_contentParsers = array(); // Configure parsers for possible Route Returns
public Route $_currentRoute; // The Route Being Dispatched
public bool $_trace = false; // For debuggin
public int $_traceVerbosity = 1; // How Verbose to trace
//SINGLETON==============================================
/**
* Empty constructor for Singleton
*/
private function __construct()
{
}
private static function newObj()
/**
* Instanciate the Singleton Object
*
* @return RouteCollection
*/
private static function newObj(): RouteCollection
{
if (!isset(self::$_routeCollection)) {
self::$_routeCollection = new RouteCollection();
@@ -40,15 +53,18 @@ class RouteCollection
self::$_routeCollection->_uri = isset($_SERVER['REQUEST_URI']) ? explode('?', $_SERVER['REQUEST_URI'])[0] : '/';
}
self::$_routeCollection->defineVerb();
self::$_routeCollection->setDefaultParsers();
}
self::$_routeCollection->_uri = str_replace("//", "/", self::$_routeCollection->_uri);
return self::$_routeCollection;
}
/**
*
* Return the Singleton instance
*
* @return RouteCollection
*/
public static function getInstance()
public static function getInstance(): RouteCollection
{
if (!isset(self::$_routeCollection)) {
return self::newObj();
@@ -56,18 +72,22 @@ class RouteCollection
return self::$_routeCollection;
}
//HELPERS================================================
//=======================================================
// HELPERS
//
/*
* Find route files with names under pathname
*
* @ctag\t RouteCollection->crawl()
/**
* Crawl the filesystem to find the System Routes
*
* @param string $basePath Path to crawl
* @param array $filenames Filenames to Match
* @return RouteCollection
*/
public function crawl($basepath = __DIR__, $filenames = array('Routes.php', 'routes.php'))
public function crawl(string $basePath = __DIR__, array $filenames = array('Routes.php', 'routes.php')): RouteCollection
{
$instance = self::getInstance();
$rdi = new \RecursiveDirectoryIterator($basepath);
$rdi = new \RecursiveDirectoryIterator($basePath);
foreach (new \RecursiveIteratorIterator($rdi) as $file) {
foreach ($filenames as $filename) {
@@ -81,11 +101,11 @@ class RouteCollection
}
/**
* Load the files
*
* Load the files with the routes
* @ctag\t RouteCollection->loadRoutes()
* @return RouteCollection
*/
function loadRoutes()
function loadRoutes(): RouteCollection
{
$instance = self::getInstance();
@@ -96,15 +116,14 @@ class RouteCollection
}
/**
*
* GET | POST | PUT | PATCH | DELETE | CLI
*
* Infer the Request Verb
*
* @param string $verb DO NOT USE
* @throws Exception
* @return void
*/
private function defineVerb()
private function defineVerb(string $verb = ""): void
{
$verb = '';
if (isset($_POST['_method'])) {
$verb = strtoupper($_POST['_method']);
} else if (php_sapi_name() == "cli") {
@@ -116,14 +135,16 @@ class RouteCollection
if (in_array($verb, self::$_verbsWhitelist)) {
$this->_verb = $verb;
} else {
echo 'Invalid Verb';
throw new Exception("Verb not Allowed");
}
}
/**
* Sort the route order
*
* @return void
*/
private function sort_routes()
private function sortRoutes(): void
{
usort($this->_routes, function ($a, $b) {
if ($a->_weight == $b->_weight) {
@@ -133,231 +154,6 @@ class RouteCollection
});
}
/**
*
*/
function submit()
{
global $ROUTE;
self::$_routeCollection->sort_routes();
$one_hit = false;
foreach (self::$_routeCollection->_routes as $route) {
if (!in_array(self::$_routeCollection->_verb, $route->_verb) && !in_array('*', $route->_verb)) {
//No it is not
continue;
}
//Request match a route
$match = $route->match(self::$_routeCollection->_uri);
if ($match) {
//Yup. Execute the route
$ROUTE = $route;
foreach ($route->_before as $key => $requestedMiddleware) {
$route->_before[$key] = array(
'callback' => $this->_middlewareSet[$key],
'params' => $requestedMiddleware
);
}
$route->_before = array_merge($this->_defaultMiddlewares, $route->_before);
$route->execute();
if (!$route->_ignore) {
$one_hit = true;
}
if ($route->_httpError) {
$this->httpError($route->_httpError);
return;
}
//If Executed, interrupt route chain?
if ($route->_block) {
break;
}
}
}
if (!$one_hit) {
$info = new \stdClass();
$info->code = 404;
$info->message = 'Not Found';
self::$_routeCollection->httpError($info);
}
}
/**
*
* @ctag RouteCollection::group('base',function(){})
*/
static function group($base = '', $callback, $name = '')
{
$collection = self::getInstance();
$collection->_groupList = [];
$collection->_groupIn = true;
$collection->_groupBase = $base;
call_user_func($callback);
$collection->_groupBase = '';
$collection->_groupIn = false;
$group = new RouteGroup($collection->_groupList);
$collection->_groups[$name] = $group;
return $group;
}
//DEFINITORS=============================================
/**
*
* @ctag RouteCollection::get('/url',function(){})
*/
static function get($uri, $callback, $weight = 0)
{
return self::add('GET', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::cli('/url',function(){})
*/
static function cli($uri, $callback, $weight = 0)
{
return self::add('CLI', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::post('/url',function(){})
*/
static function post($uri, $callback, $weight = 0)
{
return self::add('POST', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::put('/url',function(){})
*/
static function put($uri, $callback, $weight = 0)
{
return self::add('PUT', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::patch('/url',function(){})
*/
static function patch($uri, $callback, $weight = 0)
{
return self::add('PATCH', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::delete('/url',Controller@Method)
* @ctag RouteCollection::delete('/url',function(){})
*/
static function delete($uri, $callback, $weight = 0)
{
return self::add('DELETE', $uri, $callback, $weight);
}
/**
*
* @ctag RouteCollection::resource('/url',function(){})
*/
static function resource($uri)
{
throw new Exception('Not implemented');
}
/**
*
* @ctag RouteCollection::add('VERB','/url',function(){})
* @ctag RouteCollection::add('VERB','/url',Controller@Method)
*/
static function add($verb, $uri, $callback, $weight = 0)
{
$route = new Route();
if (self::getInstance()->_groupIn) {
$uri = self::getInstance()->_groupBase . $uri;
self::getInstance()->_groupList[] = &$route;
}
$uri = rtrim($uri, '/');
if (is_array($verb)) {
$route->_verb = array_map('strtoupper', $verb);
} else if (strtoupper($verb) == 'WEB') {
$route->_verb = array_merge($route->_verb, self::$_verbsWeb);
} else {
$route->_verb[] = strtoupper($verb);
}
$route->_uri = $uri;
$route->_callback[] = $callback;
$route->_weight = $weight;
$route->prepare();
self::getInstance()->_routes[] = $route;
return $route;
}
/**
*
*/
function addRoute(Route $route)
{
$this->_routes[] = $route;
}
/**
*
* @ctag RouteCollection::addDefaultMiddleware('name',function(){})
*/
static function addDefaultMiddleware($name = '', $function)
{
self::getInstance()->_defaultMiddlewares[$name] = $function;
return self::getInstance();
}
/**
*
* @ctag RouteCollection::addDefaultMiddleware('name',function(){})
*/
static function registerMiddleware($name = '', $function)
{
return self::getInstance()->_middlewareSet[$name] = $function;
}
/**
*
* @ctag RouteCollection::onHttpError(function(){})
*/
static function onHttpError($code, $function)
{
$instance = self::getInstance();
}
/**
*
* @ctag RouteCollection::httpError($class)
*/
private function httpError(\stdClass $info)
{
$info = (array) $info;
foreach ($this->_errors as $error) {
call_user_func_array($error, $info);
}
}
public static function getGroupRoutesWithTags($group, $tag = '*')
{
$instance = self::getInstance();
@@ -373,4 +169,404 @@ class RouteCollection
return $instance->_groups[$group]->getRoutesWithTag($group, $tag);
}
/**
* Trace logs
*
* @param bool $trace Should we trace
* @param int $verbosity How verbose 1 - EXECUTION | 2 - CONTENT
* @return RouteCollection
*/
public static function shouldTrace(bool $trace = true, int $verbosity = 1): RouteCollection
{
self::getInstance()->_trace = $trace;
self::getInstance()->_traceVerbosity = $verbosity;
return self::getInstance();
}
/**
* Add a trace to the log
*
* @param mixed $trace Content to Trace
* @param int $verbosity How verbose 1 - EXECUTION | 2 - CONTENT
* @return void
*/
private static function trace(mixed $trace, int $verbosity = 1): void
{
if (!self::getInstance()->_trace) {
return;
}
if ($verbosity <= self::getInstance()->_traceVerbosity) {
error_log(print_r($trace, true));
}
}
//=======================================================
// Content Parsing
//
/**
* Register some common return types
*
* @return void
*/
private function setDefaultParsers(): void
{
$this->_contentParsers["string"] = function (string $content) {
echo $content;
};
$this->_contentParsers["array"] = function (array $content) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($content);
};
$this->_contentParsers["stdClass"] = function (object $content) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($content);
};
}
/**
* Register Content Parsers
*
* @param string $type Classname of the parser
* @param callable $parser How to parse the content
* @return RouteCollection
*/
public static function addParser(string $type, callable $parser): RouteCollection
{
self::getInstance()->_contentParsers[$type] = $parser;
return self::getInstance();
}
/**
* Execute the content parsing
*
* @param mixed $content Content to be parsed
* @return void
*/
public function parseContent(mixed $content): void
{
$contentType = gettype($content);
if ($contentType == "null") { // Route didn't return anything
return;
}
if($contentType == "object"){
$contentType = get_class($content);
}
foreach (self::getInstance()->_contentParsers as $type => $parser) {
if ($contentType == $type) {
call_user_func($parser, $content);
return;
}
}
}
//=======================================================
// API
//
/**
* Create a Route Group with the prefix
*
* @param string $prefix The name prefix
* @param callable $callback The closure that will nest the routes
* @param string $name The name of the group
* @return RouteGroup
*/
public static function group(string $prefix, callable $callback, string $name = ""): RouteGroup
{
$collection = self::getInstance();
$collection->_groupList = [];
$collection->_groupIn = true;
$collection->_groupBase = $prefix;
call_user_func($callback);
$collection->_groupBase = '';
$collection->_groupIn = false;
$group = new RouteGroup($collection->_groupList);
$collection->_groups[$name] = $group;
$collection::trace("RouteGroup $prefix Added", 2);
return $group;
}
/**
* Add a GET route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function get(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('GET', $uri, $callback, $weight);
}
/**
* Add a CLI route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function cli(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('CLI', $uri, $callback, $weight);
}
/**
* Add a CLI route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function post(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('POST', $uri, $callback, $weight);
}
/**
* Add a CLI route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function put(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('PUT', $uri, $callback, $weight);
}
/**
* Add a CLI route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function patch(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('PATCH', $uri, $callback, $weight);
}
/**
* Add a CLI route
*
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function delete(string $uri, string|callable $callback, int $weight = 0): Route
{
return self::add('DELETE', $uri, $callback, $weight);
}
/**
* Add a full resource group for the prefix
*
* @param string $prefix Prefix to add the resource
* @param string $resourceNameSpace NameSpace of the class to map
* @return RouteGroup
*/
static function resource(string $prefix, string $resourceNameSpace): RouteGroup
{
throw new Exception('Not implemented');
}
/**
* Add a Route to the Collection
*
* @param Array|string $verb Verbs to match the Route
* @param string $uri URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @param int $weight Weight of the Route
* @return Route
*/
static function add(array|string $verb, string $uri, string|callable $callback, int $weight = 0): Route
{
$route = new Route();
if (self::getInstance()->_groupIn) {
$uri = self::getInstance()->_groupBase . $uri;
self::getInstance()->_groupList[] = &$route;
}
$uri = rtrim($uri, '/');
if (is_array($verb)) {
$route->_verb = array_map('strtoupper', $verb);
} else if (strtoupper($verb) == 'WEB') {
$route->_verb = array_merge($route->_verb, self::$_verbsWeb);
} else {
$route->_verb[] = strtoupper($verb);
}
$route->_uri = $uri;
$route->_callback[] = $callback;
$route->_weight = $weight;
$route->prepare();
self::getInstance()->_routes[] = $route;
self::trace("Route:: " . implode("|", $route->_verb) . " - $uri - ADDED");
return $route;
}
/**
* Add a Pre Build Route
*
* @param Route $route The route object
* @return RouteCollection
*/
function addRoute(Route $route): RouteCollection
{
$this->_routes[] = $route;
return $this;
}
/**
* Register error Pages
*
* @param int $code URI to Match
* @param string|callable $callback Callback to Execute. Might be a class or a callable
* @return RouteCollection
*/
static function error(int $code, string|callable $callback): RouteCollection
{
self::getInstance()->_errors[$code] = $callback;
return self::getInstance();
}
//=======================================================
// Middlewares
//
/**
* Register a Middleware
*
* @param string $name Middleware Name
* @param callable $function Middleware colsure
* @return RouteCollection
*/
static function registerMiddleware(string $name, callable $function): RouteCollection
{
self::getInstance()->_middlewareSet[$name] = $function;
return self::getInstance();
}
/**
* Register a middleware to match all Routes
*
* @param string $name Middleware Name
* @param callable $function Middleware colsure
* @return RouteCollection
*/
static function addDefaultMiddleware(string $name, callable $function): RouteCollection
{
self::getInstance()->_defaultMiddlewares[$name] = $function;
return self::getInstance();
}
/**
*
* @ctag RouteCollection::onHttpError(function(){})
*/
static function onHttpError($code, $function)
{
$instance = self::getInstance();
}
/**
*
* @ctag RouteCollection::httpError($class)
*/
private function httpError(stdClass $info)
{
$info = (array) $info;
foreach ($this->_errors as $error) {
call_user_func_array($error, $info);
}
}
//=======================================================
// RUN
//
/**
* Submit the request and execute the matching and Routing
*
* @return RouteCollection
*/
public function submit(): RouteCollection
{
self::$_routeCollection->sortRoutes();
$one_hit = false;
foreach (self::$_routeCollection->_routes as $route) {
if (!in_array(self::$_routeCollection->_verb, $route->_verb) && !in_array('*', $route->_verb)) {
//No it is not
continue;
}
// Check if the request match a Route
$match = $route->match(self::$_routeCollection->_uri);
if ($match) { // This Route Matches the pattern
$this->_currentRoute = $route;
foreach ($route->_before as $key => $requestedMiddleware) {
$route->_before[$key] = array(
'callback' => $this->_middlewareSet[$key],
'params' => $requestedMiddleware
);
}
$route->_before = array_merge($this->_defaultMiddlewares, $route->_before);
$routeResult = $route->execute();
if (!$route->_ignore) {
$one_hit = true;
}
if ($route->_httpError) {
$this->httpError($route->_httpError);
return $this;
}
$this->parseContent($routeResult);
if ($route->_block) { //If Executed, interrupt route chain?
break;
}
}
}
if (!$one_hit) { // Check if at least on counted Route matched
if (isset($this->_errors['404'])) { // Return a defined 404
call_user_func($this->_errors['404']);
} else { // Return a regular 404
http_response_code(404);
}
}
return $this; // Do you really want to chain more Stuff?
}
}