625 lines
18 KiB
PHP
625 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Routes;
|
|
|
|
use stdClass;
|
|
use Exception;
|
|
|
|
class RouteCollection
|
|
{
|
|
|
|
private static RouteCollection $_routeCollection;
|
|
|
|
public private(set) string $_uri = '/';
|
|
public private(set) Array $_routes = Array();
|
|
public private(set) string $_verb = '';
|
|
public private(set) Array $_loadedFiles = Array();
|
|
public private(set) Array $_errors = Array();
|
|
public private(set) Array $_defaultMiddlewares = Array();
|
|
public private(set) Array $_middlewareSet = Array();
|
|
public private(set) bool $_groupIn = false;
|
|
public private(set) string $_groupBase = '';
|
|
public private(set) Array $_groupList = Array();
|
|
public private(set) Array $_groups = Array();
|
|
public private(set) Array $_contentParsers = Array(); // Configure parsers for possible Route Returns
|
|
public private(set) Route $_currentRoute; // The Route Being Dispatched
|
|
public private(set) bool $_trace = false; // For debuggin
|
|
public private(set) int $_traceVerbosity = 1; // How Verbose to trace
|
|
public private(set) Array $_objectSpawners = Array();
|
|
|
|
private static Array $_verbsWhitelist = Array('*', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'CLI');
|
|
private static Array $_verbsWeb = Array('GET', 'POST', 'PUT', 'PATCH', 'DELETE');
|
|
|
|
//=======================================================
|
|
// SINGLETON
|
|
//
|
|
|
|
/**
|
|
* Empty constructor for Singleton
|
|
*/
|
|
private function __construct()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Instanciate the Singleton Object
|
|
*
|
|
* @return RouteCollection
|
|
*/
|
|
private static function newObj(): RouteCollection
|
|
{
|
|
if (!isset(self::$_routeCollection)) {
|
|
self::$_routeCollection = new RouteCollection();
|
|
if (php_sapi_name() == "cli") {
|
|
self::$_routeCollection->_uri = isset($_SERVER['argv'][1]) ? '/' . $_SERVER['argv'][1] : '/';
|
|
} else {
|
|
self::$_routeCollection->_uri = isset($_SERVER['REQUEST_URI']) ? explode('?', $_SERVER['REQUEST_URI'])[0] : '/';
|
|
}
|
|
|
|
self::$_routeCollection->defineVerb();
|
|
self::$_routeCollection->setDefaultParsers();
|
|
self::$_routeCollection->setDefaultObjectSpawners();
|
|
}
|
|
self::$_routeCollection->_uri = str_replace("//", "/", self::$_routeCollection->_uri);
|
|
return self::$_routeCollection;
|
|
}
|
|
|
|
/**
|
|
* Return the Singleton instance
|
|
*
|
|
* @return RouteCollection
|
|
*/
|
|
public static function getInstance(): RouteCollection
|
|
{
|
|
if (!isset(self::$_routeCollection)) {
|
|
return self::newObj();
|
|
}
|
|
return self::$_routeCollection;
|
|
}
|
|
|
|
//=======================================================
|
|
// HELPERS
|
|
//
|
|
|
|
/**
|
|
* 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(string $basePath = __DIR__, array $filenames = array('Routes.php', 'routes.php')): RouteCollection
|
|
{
|
|
$instance = self::getInstance();
|
|
|
|
$rdi = new \RecursiveDirectoryIterator($basePath);
|
|
|
|
foreach (new \RecursiveIteratorIterator($rdi) as $file) {
|
|
foreach ($filenames as $filename) {
|
|
// if (strpos($file, $filename) && $file[0] != '.') {
|
|
if (strpos($file, $filename) && !strpos($file, '.swp')) {
|
|
$instance->_loadedFiles[] = $file;
|
|
}
|
|
}
|
|
}
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Load the files
|
|
*
|
|
* @return RouteCollection
|
|
*/
|
|
function loadRoutes(): RouteCollection
|
|
{
|
|
$instance = self::getInstance();
|
|
|
|
foreach ($instance->_loadedFiles as $loadedFile) {
|
|
include $loadedFile;
|
|
}
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Infer the Request Verb
|
|
*
|
|
* @param string $verb DO NOT USE
|
|
* @throws Exception
|
|
* @return void
|
|
*/
|
|
private function defineVerb(string $verb = ""): void
|
|
{
|
|
if (isset($_POST['_method'])) {
|
|
$verb = strtoupper($_POST['_method']);
|
|
} else if (php_sapi_name() == "cli") {
|
|
$verb = "CLI";
|
|
} else {
|
|
$verb = strtoupper($_SERVER['REQUEST_METHOD']);
|
|
}
|
|
|
|
if (in_array($verb, self::$_verbsWhitelist)) {
|
|
$this->_verb = $verb;
|
|
} else {
|
|
throw new Exception("Verb not Allowed");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort the route order
|
|
*
|
|
* @return void
|
|
*/
|
|
private function sortRoutes(): void
|
|
{
|
|
usort($this->_routes, function ($a, $b) {
|
|
if ($a->_weight == $b->_weight) {
|
|
return 0;
|
|
}
|
|
return ($a->_weight < $b->_weight) ? -1 : 1;
|
|
});
|
|
}
|
|
|
|
public static function getGroupRoutesWithTags($group, $tag = '*')
|
|
{
|
|
$instance = self::getInstance();
|
|
|
|
if (!isset($instance->_groups[$group])) {
|
|
return [];
|
|
}
|
|
|
|
if (!isset($tag)) {
|
|
return $instance->_groups[$group];
|
|
}
|
|
|
|
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):void {
|
|
echo $content;
|
|
};
|
|
|
|
$this->_contentParsers["array"] = function (array $content):void {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($content);
|
|
};
|
|
|
|
$this->_contentParsers["stdClass"] = function (object $content):void {
|
|
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;
|
|
}
|
|
}
|
|
|
|
call_user_func(self::getInstance()->_contentParsers['stdClass'], $content);
|
|
}
|
|
|
|
//=======================================================
|
|
// Object Spawner
|
|
//
|
|
|
|
/**
|
|
* Get a Spawner type
|
|
*
|
|
* @param string $type
|
|
* @return mixed
|
|
*/
|
|
public static function getObjectSpawner(string $type): mixed
|
|
{
|
|
return self::getInstance()->_objectSpawners[$type];
|
|
}
|
|
|
|
/**
|
|
* Register some Object Spawner Types
|
|
*
|
|
* This will serve to be overwritten for each case so this
|
|
* library can have a default spawner and allow custom spawners
|
|
*
|
|
* @return void
|
|
*/
|
|
private function setDefaultObjectSpawners(): void
|
|
{
|
|
$this->_objectSpawners = ObjectSpawners::getDefaultSpawners();
|
|
}
|
|
|
|
/**
|
|
* Function to append a Spawner type on the list
|
|
*
|
|
* @param string $name
|
|
* @param callable $callback
|
|
* @return RouteCollection
|
|
*/
|
|
protected function addSpawner(string $name, callable $callback): RouteCollection
|
|
{
|
|
$this->_objectSpawners[$name] = $callback;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* API to append a Spawner type on the list
|
|
*
|
|
* @param string $name
|
|
* @param callable $callback
|
|
* @return RouteCollection
|
|
*/
|
|
public static function appendObjectSpawner(string $name, callable $callback): RouteCollection
|
|
{
|
|
return self::getInstance()->addSpawner($name, $callback);
|
|
}
|
|
|
|
//=======================================================
|
|
// 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|Array $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|array $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|array $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|array $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|Array $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|array $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|Array $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|array $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|Array $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|array $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|Array $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|array $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|Array $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|array $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->setVerb(array_map('strtoupper', $verb));
|
|
} else if (strtoupper($verb) == 'WEB') {
|
|
$route->setVerb(array_merge($route->_verb, self::$_verbsWeb));
|
|
} else {
|
|
$route->appendVerb(strtoupper($verb));
|
|
}
|
|
|
|
$route->setUri($uri)->setCallback($callback)->setWeight($weight)->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->setMiddlewares(array_merge($this->_defaultMiddlewares, $route->_before));
|
|
|
|
$routeResult = $route->execute();
|
|
|
|
if (!$route->_ignore) {
|
|
$one_hit = true;
|
|
}
|
|
|
|
$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?
|
|
}
|
|
|
|
}
|