_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? } }