From bc595d0a4acde06b6a453aad2a2835513924dd71 Mon Sep 17 00:00:00 2001 From: jorgecc Date: Mon, 13 Nov 2023 08:45:52 -0300 Subject: [PATCH] 2023-11-13 1.30.1 --- README.md | 7 +- examples/.htaccess | 32 +- lib/RouteOne.php | 3756 +++++++++++++-------------- lib/RouteOneCli.php | 882 +++---- lib/routeonecli | 48 +- lib/templates/htaccess_template.php | 45 +- lib/templates/route_template.php | 96 +- 7 files changed, 2439 insertions(+), 2427 deletions(-) diff --git a/README.md b/README.md index 96094e4..1aa9a5a 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ Example "path/{controller}" and "path/{controller}/{id}", the system will consid Where **default** is the optional default value. * **{controller}**: The controller (class) to call * **{action}**: The action (method) to call - * **{verb}**: The verb of the action (GET/POST,etc) + * **{verb}**: The verb of the action (GET/POST,etc.) * **{type}**: The type (value) * **{module}**: The module (value) * **{id}**: The id (value) @@ -822,7 +822,7 @@ The binary **routeonecli** is located in the vendor/bin folder ![docs/cli2.jpg](docs/cli2.jpg) -Pending means that the operation is pending to do or it requires something to configure. +Pending means that the operation is pending to do, or it requires something to configure. * enter **configure** @@ -859,6 +859,9 @@ Now, lets configure the paths ## Changelog +* 2023-11-13 1.30.1 + * fixed a bug with fetch() when the url fetched is null + * updated .htaccess, so it works better with different situations. * 2023-05-08 1.30 * addPath() now allows to specify a middleware. * 2023-04-02 1.29 diff --git a/examples/.htaccess b/examples/.htaccess index ff008e3..456c0d3 100644 --- a/examples/.htaccess +++ b/examples/.htaccess @@ -1,11 +1,21 @@ - -Options +FollowSymLinks -RewriteEngine On - -RewriteCond %{REQUEST_FILENAME} !-d -RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule ^(.*)$ router.php?req=$1 [L,QSA] - - - - + + + Options -MultiViews -Indexes + + # based in Laravel .htaccess + RewriteEngine On + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To router + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + #RewriteRule ^ index.php [L] + RewriteRule ^(.*)$ router.php?req=$1 [L] + diff --git a/lib/RouteOne.php b/lib/RouteOne.php index 0df22f0..50dba09 100644 --- a/lib/RouteOne.php +++ b/lib/RouteOne.php @@ -1,1878 +1,1878 @@ - 'api', 'ws' => 'ws', 'controller' => '']; - /** @var array[] it holds the whitelist. Ex: ['controller'=>['a1','a2','a3']] */ - protected $whitelist = [ - 'controller' => null, - 'category' => null, - 'action' => null, - 'subcategory' => null, - 'subsubcategory' => null, - 'module' => null - ]; - protected $whitelistLower = [ - 'controller' => null, - 'category' => null, - 'action' => null, - 'subcategory' => null, - 'subsubcategory' => null, - 'module' => null - ]; - /** - * @var string|null=['api','ws','controller','front'][$i] - */ - private $forceType; - private $defController; - private $defAction; - private $defCategory; - private $defSubCategory; - private $defSubSubCategory; - private $defModule; - /** @var array|bool it stores the list of path used for the modules */ - private $moduleList; - private $isModule; - /** - * - * @var string=['none','modulefront','nomodulefront'][$i] - */ - private $moduleStrategy; - private $isFetched = false; - - /** - * RouteOne constructor. - * - * @param string $base base url with or without trailing slash (it's removed if its set).
- * Example: ".","http://domain.dom", "http://domain.dom/subdomain"
- * @param string|null $forcedType =['api','ws','controller','front'][$i]
- * api then it expects a path as api/controller/action/id/idparent
- * ws then it expects a path as ws/controller/action/id/idparent
- * controller then it expects a path as controller/action/id/idparent
- * front then it expects a path as /category/subc/subsubc/id
- * @param bool|array $isModule if true then the route start reading a module name
- * false controller/action/id/idparent
- * true module/controller/action/id/idparent
- * array if the value is an array then the value is determined if the - * first - * part of the path is in the array. Example - * ['modulefolder1','modulefolder2']
- * @param bool $fetchValues (default false), if true then it also calls the method fetch() - * @param string $moduleStrategy =['none','modulefront','nomodulefront'][$i]
- * it changes the strategy to determine the type of url determined if the path - * has a module or not.
- * $forcedType must be null, otherwise this value is not calculated.
- * - */ - public function __construct(string $base = '', ?string $forcedType = null, $isModule = false, - bool $fetchValues = false, string $moduleStrategy = 'none') - { - $this->base = rtrim($base, '/'); - $this->forceType = $forcedType; - $this->moduleStrategy = $moduleStrategy; - if ($forcedType !== null) { - $this->type = $forcedType; - } - if (is_bool($isModule)) { - $this->moduleList = null; - $this->isModule = $isModule; - } else { - $this->moduleList = $isModule; - $this->isModule = false; - } - $this->isPostBack = false; - if (isset($_SERVER['REQUEST_METHOD']) && in_array($_SERVER['REQUEST_METHOD'], $this->allowedVerbs, true)) { - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $this->isPostBack = true; - } - $this->verb = $_SERVER['REQUEST_METHOD']; - } else { - $this->verb = 'GET'; - } - $this->setDefaultValues(); - if ($fetchValues) { - $this->fetch(); - } - } - - /** - * It gets the current instance of the library.
- * If the instance does not exist, then it is created
- * See constructor for the definition of the arguments. - * @param string $base - * @param string|null $forcedType - * @param bool|array $isModule - * @param bool $fetchValues - * @param string $moduleStrategy - * @return RouteOne - */ - public static function instance(string $base = '', ?string $forcedType = null, $isModule = false, - bool $fetchValues = false, string $moduleStrategy = 'none'): RouteOne - { - if (self::$instance === null) { - self::$instance = new RouteOne($base, $forcedType, $isModule, $fetchValues, $moduleStrategy); - } - return self::$instance; - } - - /** - * Returns true if there is an instance of CliOne. - * @return bool - */ - public static function hasInstance(): bool - { - return self::$instance !== null; - } - - /** - * It sets the default controller and action (if they are not entered in the route)
- * It is uses to set a default route if the value is empty or its missing.
- * Note:It must be set before fetch(). - * - * @param string $defController Default Controller - * @param string $defAction Default action - * @param string $defCategory Default category - * @param string $defSubCategory Default subcategory - * @param string $defSubSubCategory The default sub-sub-category - * @param string $defModule The default module. - * @return $this - */ - public function setDefaultValues(string $defController = '', string $defAction = '', string $defCategory = '', - string $defSubCategory = '', string $defSubSubCategory = '', - string $defModule = ''): self - { - if ($this->isFetched) { - throw new RuntimeException("RouteOne: you can't call setDefaultValues() after fetch()"); - } - $this->defController = $defController; - $this->defAction = $defAction; - $this->defCategory = $defCategory; - $this->defSubCategory = $defSubCategory; - $this->defSubSubCategory = $defSubSubCategory; - $this->defModule = $defModule; - return $this; - } - - /** - * It clears all the paths defined - * - * @return void - */ - public function clearPath(): void - { - $this->path = []; - $this->pathName = []; - $this->middleWare = []; - } - /** - * It adds a paths that could be evaluated using fetchPath()
- * Example:
- *
-     * $this->addPath('api/{controller}/{action}/{id:0}','apipath');
-     * $this->addPath('/api/{controller}/{action}/{id:0}/','apipath'); // "/" at the beginner and end are trimmed.
-     * $this->addPath('{controller}/{action}/{id:0}','webpath');
-     * $this->addPath('{controller:root}/{action}/{id:0}','webpath'); // root path using default
-     * $this->addPath('somepath','namepath',
-     *      function(callable $next,$id=null,$idparent=null,$event=null) {
-     *          echo "middleware\n";
-     *          $result=$next($id,$idparent,$event); // calling the controller
-     *          echo "endmiddleware\n";
-     *           return $result;
-     *       });
-     * 
- * Note:
- * The first part of the path, before the "{" is used to determine which path will be used.
- * Example "path/{controller}" and "path/{controller}/{id}", the system will consider that are the same path - * @param string $path The path, example "aaa/{controller}/{action:default}/{id}"
- * Where default is the optional default value. - * - * @param string|null $name (optional), the name of the path - * @param callable|null $middleWare A callable function used for middleware.
- * The first argument of the function must be a callable method
- * The next arguments must be the arguments defined by callObjectEx - * (id,idparent,event) - * @return $this - */ - public function addPath(string $path, ?string $name = null, ?callable $middleWare = null): RouteOne - { - if (!$path) { - throw new RuntimeException('Path must not be empty, use default value to set a root path'); - } - $path = trim($path, '/'); - $x0 = strpos($path, '{'); - if ($x0 === false) { - $partStr = ''; - $base = $path; - } else { - $base = substr($path, 0, $x0); - $partStr = substr($path, $x0); - } - $items = explode('/', $partStr); - $itemArr = []; - foreach ($items as $v) { - $p = trim($v, '{}' . " \t\n\r\0\x0B"); - $itemAdd = explode(':', $p, 2); - if (count($itemAdd) === 1) { - $itemAdd[] = null; // add a default value - } - $itemArr[] = $itemAdd; - } - if ($name === null) { - $this->pathName[] = $base; - $this->path[] = $itemArr; - $this->middleWare[] = $middleWare; - } else { - $this->pathName[$name] = $base; - $this->path[$name] = $itemArr; - $this->middleWare[$name] = $middleWare; - } - return $this; - } - - /** - * It fetches the path previously defined by addPath. - * - * @return int|string|null return null if not path is evaluated,
- * otherwise, it returns the number/name of the path. It could return the value 0 (first path) - * @noinspection NotOptimalRegularExpressionsInspection - */ - public function fetchPath() - { - $this->lastError = []; - $this->currentPath = null; - $urlFetchedOriginal = $this->getUrlFetchedOriginal(); - $this->queries = $_GET; - unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); - foreach ($this->path as $pnum => $pattern) { - if ($this->pathName[$pnum] !== '' && strpos($urlFetchedOriginal ?? '', $this->pathName[$pnum]) !== 0) { - // basePath url does not match. - $this->lastError[$pnum] = "Pattern [$pnum], base url does not match"; - continue; - } - $urlFetched = substr($urlFetchedOriginal ?? '', strlen($this->pathName[$pnum])); - // nginx returns a path as /aaa/bbb apache aaa/bbb - if ($urlFetched !== '') { - $urlFetched = ltrim($urlFetched, '/'); - } - $path = $this->getExtracted($urlFetched); - foreach ($this->path[$pnum] as $key => $v) { - if ($v[1] === null) { - if (!array_key_exists($key, $path) || !isset($path[$key])) { - // the field is required but there we don't find any value - $this->lastError[$pnum] = "Pattern [$pnum] required field ($v[0]) not found in url"; - continue; - } - $name = $v[0]; - $value = $path[$key]; - } else { - $name = $v[0]; - if (isset($path[$key]) && $path[$key]) { - $value = $path[$key]; - } else { - // value not found, set default value - $value = $v[1]; - } - } - // 'controller', 'action', 'verb', 'event', 'type', 'module', 'id', 'idparent', 'category' - // , 'subcategory', 'subsubcategory' - switch ($name) { - case 'controller': - $this->controller = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case 'action': - $this->action = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case 'module': - $this->module = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case 'id': - $this->id = $value; - break; - case 'idparent': - $this->idparent = $value; - break; - case 'category': - $this->category = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case 'subcategory': - $this->subcategory = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case 'subsubcategory': - $this->subsubcategory = preg_replace('/[^a-zA-Z0-9_]/', "", $value); - break; - case '': - break; - default: - throw new RuntimeException("pattern incorrect [$name:$value]"); - } - } - $this->event = $this->getRequest('_event'); - $this->extra = $this->getRequest('_extra'); - $this->currentPath = $pnum; - break; - } - return $this->currentPath; - } - - /** - * - * It uses the next strategy to obtain the parameters;
- * - */ - public function fetch(): void - { - //$urlFetched = $_GET['req'] ?? null; // controller/action/id/.. - $urlFetched = $this->getUrlFetchedOriginal(); // // controller/action/id/.. - $this->isFetched = true; - unset($_GET[$this->argumentName]); - /** @noinspection HostnameSubstitutionInspection */ - $this->httpHost = $_SERVER['HTTP_HOST'] ?? ''; - $this->requestUri = $_SERVER['REQUEST_URI'] ?? ''; - // nginx returns a path as /aaa/bbb apache aaa/bbb - if ($urlFetched !== '') { - $urlFetched = ltrim($urlFetched, '/'); - } - $this->queries = $_GET; - unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); - $path = $this->getExtracted($urlFetched, true); - //$first = $path[0] ?? $this->defController; - if (isset($path[0]) && $this->moduleList !== null) { - // if moduleArray has values then we find if the current path is a module or not. - $this->isModule = in_array($path[0], $this->moduleList, true); - } - $id = 0; - if ($this->isModule) { - $this->module = $path[$id] ?? $this->defModule; - $id++; - if ($this->moduleStrategy === 'modulefront') { - // the path is not a module, then type is set as front. - $this->type = 'front'; - } - } else { - $this->module = $this->defModule; - if ($this->moduleStrategy === 'nomodulefront') { - // the path is not a module, then type is set as front. - $this->type = 'front'; - } - } - if ($this->forceType !== null) { - $this->type = $this->forceType; - } - if (!$this->type) { - $this->type = 'controller'; - $this->setController((!$path[$id]) ? $this->defController : $path[$id]); - $id++; - } - switch ($this->type) { - case 'ws': - case 'api': - //$id++; [fixed] - $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); - $id++; - break; - case 'controller': - $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); - $id++; - break; - case 'front': - // it is processed differently. - $this->setCategory(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defCategory); - $id++; - $this->subcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubCategory; - $id++; - $this->subsubcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubSubCategory; - /** @noinspection PhpUnusedLocalVariableInspection */ - $id++; - $this->id = end($path); // id is the last element of the path - $this->event = $this->getRequest('_event'); - $this->extra = $this->getRequest('_extra'); - return; - } - $this->action = $path[$id] ?? null; - $id++; - $this->action = $this->action ?: $this->defAction; // $this->action is never undefined, so we don't need isset - $this->id = $path[$id] ?? null; - $id++; - $this->idparent = $path[$id] ?? null; - /** @noinspection PhpUnusedLocalVariableInspection */ - $id++; - $this->event = $this->getRequest('_event'); - $this->extra = $this->getRequest('_extra'); - } - - /** - * @param string $search - * @param array|string $replace - * @param string $subject - * @param int $limit - * @return string - */ - protected function str_replace_ex(string $search, $replace, string $subject, int $limit = 99999): string - { - return implode($replace, explode($search, $subject, $limit + 1)); - } - - /** - * It is an associative array with the allowed paths or null (default behaviour) to allows any path.
- * The comparison ignores cases but the usage is "case-sensitive" and it uses the case used here
- * For example: if we allowed the controller called "Controller1" then:
- * - * Example: - *
-     * // we only want to allow the controllers called Purchase, Invoice and Customer.
-     * $this->setWhiteList('controller',['Purchase','Invoice','Customer']);
-     * 
- * Note: this must be executed before fetch() - * @param string $type =['controller','category','action','subcategory','subsubcategory','module'][$i] - * @param array|null $array if null (default value) then we don't validate the information. - */ - public function setWhiteList(string $type, ?array $array): void - { - if ($this->isFetched && $array !== null) { - throw new RuntimeException("RouteOne: you can't call setWhiteList() after fetch()"); - } - $type = strtolower($type); - $this->whitelist[$type] = $array; - $this->whitelistLower[$type] = is_array($array) ? array_map('strtolower', $array) : null; - } - - /** - * If the subdomain is empty or different to www, then it redirects to www.domain.com.
- * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
- * Note: If this code needs to redirect, then it stops the execution of the code. Usually, - * it must be called at the top of the code - * - * @param bool $https If true then it also redirects to https - * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new full url - * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
- * Otherwise, if $redirect=false, it returns the full url to redirect. - */ - public function alwaysWWW(bool $https = false, bool $redirect = true): ?string - { - $url = $this->httpHost; - //if (strpos($url, '.') === false || ip2long($url)) { - //} - if (strpos($url ?? '', 'www.') === false) { - $location = $this->getLocation($https); - $location .= '//www.' . $url . $this->requestUri; - if ($redirect) { - header('HTTP/1.1 301 Moved Permanently'); - header('Location: ' . $location); - if (http_response_code()) { - die(1); - } - } - return $location; - } - if ($https) { - return $this->alwaysHTTPS(false); - } - return null; - } - - private function getLocation($https): string - { - if ($https) { - $port = $_SERVER['HTTP_PORT'] ?? '443'; - $location = 'https:'; - if ($port !== '443' && $port !== '80') { - $location .= $port; - } - } else { - $port = $_SERVER['HTTP_PORT'] ?? '80'; - $location = 'http:'; - if ($port !== '80') { - $location .= $port; - } - } - return $location; - } - - /** - * If the page is loaded as http, then it redirects to https
- * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
- * Note: If this code needs to redirect, then it stops the execution of the code. Usually, - * it must be called at the top of the code - * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url - * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
- * Otherwise, if $redirect=false, it returns the url to redirect. - */ - public function alwaysHTTPS(bool $redirect = true): ?string - { - if (strpos($this->httpHost, '.') === false || ip2long($this->httpHost)) { - return null; - } - $https = $_SERVER['HTTPS'] ?? ''; - if (empty($https) || $https === 'off') { - $port = $_SERVER['HTTP_PORT'] ?? '443'; - $port = ($port === '443' || $port === '80') ? '' : $port; - $location = 'https:' . $port . '//' . $this->httpHost . $this->requestUri; - if ($redirect) { - header('HTTP/1.1 301 Moved Permanently'); - header('Location: ' . $location); - if (http_response_code()) { - die(1); - } - } - return $location; - } - return null; - } - - /** - * If the subdomain is www (example www.domain.dom) then it redirects to a naked domain "domain.dom"
- * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
- * Note: If this code needs to redirect, then we should stop the execution of any other code. Usually, - * it must be called at the top of the code - * - * @param bool $https If true then it also redirects to https - * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url - * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
- * Otherwise, if $redirect=false, it returns the full url to redirect. - */ - public function alwaysNakedDomain(bool $https = false, bool $redirect = true): ?string - { - $url = $this->httpHost; - if (strpos($url ?? '', 'www.') === 0) { - $host = substr($url, 4); // we remove the www. at first - $location = $this->getLocation($https); - $location .= '//' . $host . $this->requestUri; - if ($redirect) { - header('HTTP/1.1 301 Moved Permanently'); - header('Location: ' . $location); - if (http_response_code()) { - die(1); - } - return ''; - } - return $location; - } - if ($https) { - return $this->alwaysHTTPS(false); - } - return null; - } - - /** - * It creates and object (for example, a Controller object) and calls the method.
- * Example: (type controller,api,ws) - *
-     * $this->callObject('cocacola\controller\%sController'); // %s is replaced by the name of the current controller
-     * $this->callObject('namespace/%2s/%1sClass'); // it calls namespace/Module/ExampleClass (only if module is able)
-     * $this->callObject('namespace/%2s/%3s%/%1sClass'); // %3s is for the type of path
-     * 
- * Note: The method called should be written as (static or not)
- *
-     * public function *nameaction*Action($id="",$idparent="",$event="") { }
-     * 
- * - * @param string $classStructure structure of the class.
- * Type=controller,api,ws
- * The first %s (or %1s) is the name of the controller.
- * The second %s (or %2s) is the name of the module (if any and if - * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. - * controller,api,ws,front)
- * Type=front
- * The first %s (or %1s) is the name of the category.
- * The second %s (or %2s) is the name of the subcategory
- * The third %s (or %3s) is the type of the subsubcategory
- * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If false then it - * returns the error (if any) - * @param string $method [optional] Default value='%sAction'. The name of the method to call (get/post). - * If method does not exist then it will use $methodGet or $methodPost - * @param string $methodGet [optional] Default value='%sAction'. The name of the method to call (get) but only - * if the method defined by $method is not defined. - * @param string $methodPost [optional] Default value='%sAction'. The name of the method to call (post) but - * only - * if the method defined by $method is not defined. - * @param array $arguments [optional] Default value=['id','idparent','event'] the arguments to pass to the - * function - * - * @return string|null null if the operation was correct, or the message of error if it failed. - * @throws Exception - * @deprecated - * @see self::callObjectEx Use callObjectEx('{controller}Controller'); instead of callObject('%sController); - */ - public function callObject( - string $classStructure = '%sController', bool $throwOnError = true, - string $method = '%sAction', string $methodGet = '%sActionGet', - string $methodPost = '%sActionPost', - array $arguments = ['id', 'idparent', 'event'] - ): ?string - { - if ($this->notAllowed === true) { - throw new UnexpectedValueException('Input is not allowed'); - } - if ($this->type !== 'front') { - if ($this->controller === null) { - throw new UnexpectedValueException('Controller is not set or it is not allowed'); - } - $op = sprintf($classStructure, $this->controller, $this->module, $this->type); - } else { - $op = sprintf($classStructure, $this->category, $this->subcategory, $this->subsubcategory); - } - if (!class_exists($op)) { - if ($throwOnError) { - throw new RuntimeException("Class $op doesn't exist"); - } - return "Class $op doesn't exist"; - } - try { - $controller = new $op(); - if ($this->type !== 'front') { - $actionRequest = sprintf($method, $this->action); - } else { - /** @noinspection PrintfScanfArgumentsInspection */ - $actionRequest = sprintf($method, $this->subcategory, $this->subsubcategory); - } - $actionGetPost = (!$this->isPostBack) ? sprintf($methodGet, $this->action) - : sprintf($methodPost, $this->action); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - $args = []; - foreach ($arguments as $a) { - $args[] = $this->{$a}; - } - if (method_exists($controller, $actionRequest)) { - try { - $controller->{$actionRequest}(...$args); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - } elseif (method_exists($controller, $actionGetPost)) { - try { - $controller->{$actionGetPost}(...$args); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - } else { - $pb = $this->isPostBack ? '(POST)' : '(GET)'; - $msgError = "Action [$actionRequest or $actionGetPost] $pb not found for class [$op]"; - $msgError = strip_tags($msgError); - if ($throwOnError) { - throw new UnexpectedValueException($msgError); - } - return $msgError; - } - return null; - } - - /** - * Get multiples values, get, post, request, header, etc. - * @param string $key The name of the key to read.
- * Body and verb do not use a key. - * @param string $type =['get','post','request','header','body','verb'][$i] - * @param mixed|null $defaultValue the default value if the value is not found.
- * It is ignored by body and verb because both always returns a value - * @return false|mixed|string|null - * @throws RuntimeException - */ - public function getMultiple(string $key, string $type, $defaultValue = null) - { - switch ($type) { - case 'get': - $r = $this->getQuery($key, $defaultValue); - break; - case 'post': - $r = $this->getPost($key, $defaultValue); - break; - case 'request': - $r = $this->getRequest($key, $defaultValue); - break; - case 'header': - $r = $this->getHeader($key, $defaultValue); - break; - case 'body': - $r = $this->getBody(true); - $r = $r === false ? $defaultValue : $r; - break; - case 'verb': - $r = $this->verb; - break; - default: - throw new RuntimeException("argument incorrect, type [$type] unknown"); - } - return $r; - } - - /** - * It creates and object (for example, a Controller object) and calls the method.
- * Note: It is an advanced version of this::callObject()
- * This method uses {} to replace values.
- * - * Note: You can also convert the case - * - * Example:
- *
-     * // controller example http://somedomain/Customer/Insert/23
-     * $this->callObjectEx('cola\controller\{controller}Controller');
-     * // it calls the method cola\controller\Customer::InsertAction(23,'','');
-     *
-     * $this->callObjectEx('cola\controller\{controller}Controller','{action}Action{verb}');
-     * // it calls the method cola\controller\Customer::InsertActionGet(23,'',''); or InsertActionPost, etc.
-     *
-     * // front example: http://somedomain/product/coffee/nescafe/1
-     * $this->callObjectEx('cocacola\controller\{category}Controller',false,'{subcategory}',null
-     *                     ,null,['subsubcategory','id']);
-     * // it calls the method cocacola\controller\product::coffee('nescafe','1');
-     *
-     * // callable instead of a class
-     * $this->callObjectEx(function($id,$idparent,$event) { echo "hi"; });
-     * 
- * - * @param string|object|callable $classStructure [optional] Default value='{controller}Controller'.
- * If classStructure is an string then it must indicate the - * full name of the class including namespaces - * (SomeClassController::class is allowed)
- * if $classStructure is an object, - * then it uses the instance of it
- * if $classStructure is a callable, then it calls the - * function. The arguments are defined by $arguments
- * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If - * false then it returns the error as a string (if any) - * @param string|null $method [optional] Default value='{action}Action'. The name of the method - * to call - * (get/post). If the method does not exist then it will use - * $methodGet - * (isPostBack=false) or $methodPost (isPostBack=true) - * @param string|null $methodGet [optional] Default value='{action}Action{verb}'. The name of the - * method to call when get - * (get) but only if the method defined by $method is not defined. - * @param string|null $methodPost [optional] Default value='{action}Action{verb}'. The name of the - * method to call - * (post) but only if the method defined by $method is not defined. - * @param array $arguments [optional] Default value=['id','idparent','event']
- * Values allowed:'get','post','request','header','body','verb'
- * T The arguments to - * pass to the methods and middleware
- * Example
- * - * @param array $injectArguments [optional] You can inject values into the argument of the - * instance's constructor.
It will do nothing if you pass an - * object as - * $classStructure. - * @param string $onlyPath default is "*"(any path), if set, then this method will only work - * if the path - * (obtained by fetchPath) is the indicated here. - * @return string|null Returns a string with an error or null if not error. - * If $classStructure is callable, then it returns the value of the - * function. - * @throws Exception - */ - public function callObjectEx( - $classStructure = '{controller}Controller', bool $throwOnError = true, - ?string $method = '{action}Action', ?string $methodGet = '{action}Action{verb}', - ?string $methodPost = '{action}Action{verb}', array $arguments = ['id', 'idparent', 'event'], - array $injectArguments = [], - string $onlyPath = '*' - ): ?string - { - if ($onlyPath !== '*' && $this->currentPath !== $onlyPath) { - // This object must be called using a specific path. - return null; - } - if ($this->notAllowed === true) { - throw new UnexpectedValueException('Input method is not allowed', 403); - } - if (is_object($classStructure)) { - $className = get_class($classStructure); - } else if (is_callable($classStructure)) { - $className = '**CALLABLE**'; - } else { - $className = $this->replaceNamed($classStructure); - } - if (!class_exists($className) && $className !== '**CALLABLE**') { - if ($throwOnError) { - throw new RuntimeException("Class $className doesn't exist", 404); - } - return "Class $className doesn't exist"; - } - $args = []; - foreach ($arguments as $keyArg => $valueArg) { - if (in_array($valueArg, $this->allowedFields, true)) { - $args[$keyArg] = $this->{$valueArg}; - } else if (is_string($valueArg) || is_numeric($valueArg)) { - $x = explode(':', $valueArg, 3); // get:fieldname:defaultvalue - if (count($x) < 2) { - $msg = 'RouteOne::callObjectEx, argument incorrect, use type:name:default or a defined name'; - if ($throwOnError) { - throw new RuntimeException($msg); - } - return $msg; - } - try { - $args[$keyArg] = $this->getMultiple($x[1], $x[0], $x[2] ?? null); - } catch (Exception $ex) { - if ($throwOnError) { - throw new RuntimeException($ex->getMessage()); - } - return $ex->getMessage(); - } - } else { - // ['field'=>$someobjectorarray] or [$someobjectorarray] - $args[$keyArg] = $valueArg; - } - } - try { - if (is_callable($classStructure)) { - if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { - return $this->middleWare[$this->currentPath]($classStructure, ...$args); - } - return $classStructure(...$args); - } - if (is_object($classStructure)) { - $controller = $classStructure; - } else if(method_exists($className,'getInstance')) { - $controller=$className->getInstance(); // try to autowire an instance. - } elseif(method_exists($className,'instance')) { - $controller=$className->instance(); // try to autowire an instance - } else { - $controller = new $className(...$injectArguments); // try to create a new controller. - } - $actionRequest = $this->replaceNamed($method); - $actionGetPost = (!$this->isPostBack) ? $this->replaceNamed($methodGet) - : $this->replaceNamed($methodPost); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - if (method_exists($controller, $actionRequest)) { - /** @noinspection DuplicatedCode */ - try { - //$call = $controller->{$actionRequest}; - if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { - return $this->middleWare[$this->currentPath]( - static function(...$args) use ($controller,$actionRequest) { // it is a wrapper function - return $controller->{$actionRequest}(...$args); - } - , ...$args); - } - $controller->{$actionRequest}(...$args); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - } elseif (method_exists($controller, $actionGetPost)) { - /** @noinspection DuplicatedCode */ - try { - if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { - //return $this->middleWare[$this->currentPath]($call, ...$args); - return $this->middleWare[$this->currentPath]( - static function(...$args) use ($controller,$actionGetPost) { // it is a wrapper function - return $controller->{$actionGetPost}(...$args); - } - , ...$args); - } - $controller->{$actionGetPost}(...$args); - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - } else { - $pb = $this->isPostBack ? '(POST)' : '(GET)'; - $msgError = "Action ex [$actionRequest or $actionGetPost] $pb not found for class [$className]"; - $msgError = strip_tags($msgError); - if ($throwOnError) { - throw new UnexpectedValueException($msgError, 400); - } - return $msgError; - } - return null; - } - - /** - * Return a formatted string like vsprintf() with named placeholders.
- * When a placeholder doesn't have a matching key (it's not in the whitelist $allowedFields), then the value - * is not modified, and it is returned as is.
- * If the name starts with uc_,lc_,u_,l_ then it is converted into ucfirst,lcfirst,uppercase or lowercase. - * - * @param string|null $format - * - * @return string - */ - private function replaceNamed(?string $format): string - { - if ($format === null) { - return ''; - } - return preg_replace_callback("/{(\w+)}/", function($matches) { - $nameField = $matches[1]; - $result = ''; - if (strpos($nameField ?? '', '_') > 0) { - [$x, $nf] = explode('_', $nameField, 2); - if (in_array($nf, $this->allowedFields, true) === false) { - return '{' . $nameField . '}'; - } - switch ($x) { - case 'uc': - $result = ucfirst(strtolower($this->{$nf})); - break; - case 'lc': - $result = lcfirst(strtoupper($this->{$nf})); - break; - case 'u': - $result = strtoupper($this->{$nf}); - break; - case 'l': - $result = strtolower($this->{$nf}); - break; - } - } else { - if (in_array($nameField, $this->allowedFields, true) === false) { - return '{' . $nameField . '}'; - } - $result = $this->{$nameField}; - } - return $result; - }, $format); - } - - /** - * It calls (include) a file using the current controller. - * - * @param string $fileStructure It uses sprintf
- * The first %s (or %1s) is the name of the controller.
- * The second %s (or %2s) is the name of the module (if any and if - * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. - * controller,api,ws,front)
Example %s.php => controllername.php
Example - * %s3s%/%1s.php => controller/controllername.php - * @param bool $throwOnError - * - * @return string|null - * @throws Exception - */ - public function callFile(string $fileStructure = '%s.php', bool $throwOnError = true): ?string - { - $op = sprintf($fileStructure, $this->controller, $this->module, $this->type); - try { - include $op; - } catch (Exception $ex) { - if ($throwOnError) { - throw $ex; - } - return $ex->getMessage(); - } - return null; - } - - /** - * Returns the current base url without traling space, paremters or queries/b
- * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and - * it could be modified by the user. - * - * @param bool $withoutFilename if true then it doesn't include the filename - * - * @return string - */ - public function getCurrentUrl(bool $withoutFilename = true): string - { - $sn = $_SERVER['SCRIPT_NAME'] ?? ''; - if ($withoutFilename) { - return dirname($this->getCurrentServer() . $sn); - } - return $this->getCurrentServer() . $sn; - } - - /** - * It returns the current server without trailing slash.
- * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and - * it could be modified by the user. - * - * @return string - */ - public function getCurrentServer(): string - { - $server_name = $this->serverName ?? $_SERVER['SERVER_NAME'] ?? null; - $c = filter_var($server_name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); - $server_name = $c ? $server_name : $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; - $sp = $_SERVER['SERVER_PORT'] ?? 80; - $port = !in_array($sp, ['80', '443'], true) ? ':' . $sp : ''; - $https = $_SERVER['HTTPS'] ?? ''; - $scheme = !empty($https) && (strtolower($https) === 'on' || $https === '1') ? 'https' : 'http'; - return $scheme . '://' . $server_name . $port; - } - - /** - * It sets the current server name. It is used by getCurrentUrl() and getCurrentServer() - * - * @param string $serverName Example: "localhost", "127.0.0.1", "www.site.com", etc. - * - * @see RouteOne::getCurrentUrl - * @see RouteOne::getCurrentServer - */ - public function setCurrentServer(string $serverName): void - { - $this->serverName = $serverName; - } - - /** - * It sets the values of the route using customer values
- * If the values are null, then it keeps the current values (if any) - * - * @param null|string $module Name of the module - * @param null|string $controller Name of the controller. - * @param null|string $action Name of the action - * @param null|string $id Name of the id - * @param null|string $idparent Name of the idparent - * @param null|string $category Value of the category - * @param string|null $subcategory Value of the subcategory - * @param string|null $subsubcategory Value of the sub-subcategory - * @return $this - */ - public function url( - ?string $module = null, - ?string $controller = null, - ?string $action = null, - ?string $id = null, - ?string $idparent = null, - ?string $category = null, - ?string $subcategory = null, - ?string $subsubcategory = null - ): self - { - if ($module !== null) { - $this->module = $module; - } - if ($controller !== null) { - $this->setController($controller); - } - if ($action !== null) { - $this->action = $action; - } - if ($id !== null) { - $this->id = $id; - } - if ($idparent !== null) { - $this->idparent = $idparent; - } - if ($category !== null) { - $this->category = $category; - } - if ($subcategory !== null) { - $this->subcategory = $subcategory; - } - if ($subsubcategory !== null) { - $this->subsubcategory = $subsubcategory; - } - $this->extra = null; - $this->event = null; - return $this; - } - - public function urlFront( - $module = null, - $category = null, - $subcategory = null, - $subsubcategory = null, - $id = null - ): RouteOne - { - if ($module) { - $this->module = $module; - } - if ($category) { - $this->setCategory($category); - } - if ($subcategory) { - $this->subcategory = $subcategory; - } - if ($subsubcategory) { - $this->subsubcategory = $subsubcategory; - } - if ($id) { - $this->id = $id; - } - $this->extra = null; - $this->event = null; - return $this; - } - - // .htaccess: - // RewriteRule ^(.*)$ index.php?req=$1 [L,QSA] - public function reset(): RouteOne - { - // $this->base=''; base is always keep - $this->isFetched = false; - $this->defController = ''; - $this->defCategory = ''; - $this->defSubCategory = ''; - $this->defSubSubCategory = ''; - $this->defModule = ''; - $this->forceType = null; - $this->defAction = ''; - $this->isModule = ''; - $this->moduleStrategy = 'none'; - $this->moduleList = null; - $this->id = null; - $this->event = null; - $this->idparent = null; - $this->extra = null; - $this->verb = 'GET'; - $this->notAllowed = false; - $this->clearPath(); - $this->currentPath = null; - return $this; - } - - /** - * This function is used to identify the type automatically. If the url is empty then it is marked as default
- * It returns the first one that matches. - * Example:
- *
-     * $this->setIdentifyType([
-     *      'controller' =>'backend', // domain.dom/backend/controller/action => controller type
-     *      'api'=>'api',             // domain.dom/api/controller => api type
-     *      'ws'=>'api/ws'            // domain.dom/api/ws/controller => ws type
-     *      'front'=>''               // domain.dom/* =>front (any other that does not match)
-     * ]);
-     * 
- * - * @param $array - */ - public function setIdentifyType($array): void - { - $this->identify = $array; - } - - /** - * It returns a non route url based in the base url.
- * Example:
- * $this->getNonRouteUrl('login.php'); // http://baseurl.com/login.php - * - * @param string $urlPart - * - * @return string - * @see RouteOne - */ - public function getNonRouteUrl(string $urlPart): string - { - return $this->base . '/' . $urlPart; - } - - /** - * It reconstructs an url using the current information.
- * Example:
- *
-     * $currenturl=$this->getUrl();
-     * $buildurl=$this->url('mod','controller','action',20)->getUrl();
-     * 
- * Note:. It discards any information outside the values pre-defined - * (example: /controller/action/id/idparent/?arg=1&arg=2)
- * It does not consider the path() structure but the type of url. - * @param string $extraQuery If we want to add extra queries - * @param bool $includeQuery If true then it includes the queries in $this->queries - * - * @return string - */ - public function getUrl(string $extraQuery = '', bool $includeQuery = false): string - { - $url = $this->base . '/'; - if ($this->isModule) { - $url .= $this->module . '/'; - } - switch ($this->type) { - case 'ws': - case 'controller': - case 'api': - $url .= ''; - break; - case 'front': - $url .= "$this->category/$this->subcategory/$this->subsubcategory/"; - if ($this->id) { - $url .= $this->id . '/'; - } - if ($this->idparent) { - $url .= $this->idparent . '/'; - } - return $url; - default: - trigger_error('type [' . $this->type . '] not defined'); - break; - } - $url .= $this->controller . '/'; // Controller is always visible, even if it is empty - $url .= $this->action . '/'; // action is always visible, even if it is empty - $sepQuery = '?'; - if (($this->id !== null && $this->id !== '') || $this->idparent !== null) { - $url .= $this->id . '/'; // id is visible if id is not empty or if idparent is not empty. - } - if ($this->idparent !== null && $this->idparent !== '') { - $url .= $this->idparent . '/'; // idparent is only visible if it is not empty (zero is not empty) - } - if ($this->event !== null && $this->event !== '') { - $url .= '?_event=' . $this->event; - $sepQuery = '&'; - } - if ($this->extra !== null && $this->extra !== '') { - $url .= $sepQuery . '_extra=' . $this->extra; - $sepQuery = '&'; - } - if ($extraQuery !== null && $extraQuery !== '') { - $url .= $sepQuery . $extraQuery; - $sepQuery = '&'; - } - if ($includeQuery && count($this->queries)) { - $url .= $sepQuery . http_build_query($this->queries); - } - return $url; - } - - /** - * Returns the url using the path and current values
- * The trail "/" is always removed. - * @param string|null $idPath If null then it uses the current path obtained by fetchUrl()
- * If not null, then it uses the id path to obtain the path. - * @return string - */ - public function getUrlPath(?string $idPath = null): string - { - $idPath = $idPath ?? $this->currentPath; - if (!isset($this->path[$idPath])) { - throw new RuntimeException("Path $idPath not defined"); - } - $patternItems = $this->path[$idPath]; - $url = $this->base . '/' . $this->pathName[$idPath]; - $final = []; - foreach ($patternItems as $vArr) { - [$idx, $def] = $vArr; - $value = $this->{$idx} ?? $def; - $final[] = $value; - } - $url .= implode('/', $final); - return rtrim($url, '/'); - } - - /** - * It returns the current type. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * It returns the current name of the module - * - * @return string - */ - public function getModule(): string - { - return $this->module; - } - - /** - * @param string $key - * @param null|mixed $valueIfNotFound - * - * @return mixed - */ - public function getQuery(string $key, $valueIfNotFound = null) - { - return $this->queries[$key] ?? $valueIfNotFound; - } - - /** - * It gets the current header (if any) - * - * @param string $key The key to read - * @param null|mixed $valueIfNotFound - * @return mixed|null - */ - public function getHeader(string $key, $valueIfNotFound = null) - { - $keyname = 'HTTP_' . strtoupper($key); - return $_SERVER[$keyname] ?? $valueIfNotFound; - } - - /** - * It gets the Post value if not the Get value - * - * @param string $key The key to read - * @param null|mixed $valueIfNotFound - * @return mixed|null - */ - public function getRequest(string $key, $valueIfNotFound = null) - { - return $_POST[$key] ?? $_GET[$key] ?? $valueIfNotFound; - } - - /** - * It gets the Post value or returns the default value if not found - * - * @param string $key The key to read - * @param null|mixed $valueIfNotFound - * @return mixed|null - */ - public function getPost(string $key, $valueIfNotFound = null) - { - return $_POST[$key] ?? $valueIfNotFound; - } - - /** - * It gets the Get (url parameter) value or returns the default value if not found - * - * @param string $key The key to read - * @param null|mixed $valueIfNotFound - * @return mixed|null - */ - public function getGet(string $key, $valueIfNotFound = null) - { - return $_GET[$key] ?? $valueIfNotFound; - } - - /** - * It gets the body of a request. - * - * @param bool $jsonDeserialize if true then it de-serialize the values. - * @param bool $asAssociative if true (default value) then it returns as an associative array. - * @return false|mixed|string - */ - public function getBody(bool $jsonDeserialize = false, bool $asAssociative = true) - { - $entityBody = file_get_contents('php://input'); - if (!$jsonDeserialize) { - return $entityBody; - } - return json_decode($entityBody, $asAssociative); - } - - /** - * It sets a query value - * - * @param string $key - * @param null|mixed $value - */ - public function setQuery(string $key, $value): void - { - $this->queries[$key] = $value; - } - - /** - * It returns the current name of the controller. - * - * @return string|null - */ - public function getController(): ?string - { - return $this->controller; - } - - /** - * - * @param $controller - * - * @return RouteOne - */ - public function setController($controller): RouteOne - { - if (is_array($this->whitelist['controller'])) { // there is a whitelist - if (in_array(strtolower($controller), $this->whitelistLower['controller'], true)) { - $p = array_search($controller, $this->whitelistLower['controller'], true); - $this->controller = $this->whitelist['controller'][$p]; // we returned the same value but with the right case. - return $this; - } - // and this value is not found there. - $this->controller = $this->defController; - $this->notAllowed = true; - return $this; - } - $this->controller = $controller; - return $this; - } - - /** - * - * - * @return string|null - */ - public function getAction(): ?string - { - return $this->action; - } - - /** - * - * - * @param $action - * - * @return RouteOne - */ - public function setAction($action): RouteOne - { - $this->action = $action; - return $this; - } - - /** - * - * - * @return string - */ - public function getId(): string - { - return $this->id; - } - - /** - * - * - * @param $id - * - * @return RouteOne - */ - public function setId($id): RouteOne - { - $this->id = $id; - return $this; - } - - /** - * - * - * @return string - */ - public function getEvent(): string - { - return $this->event; - } - - /** - * - * - * @param $event - * - * @return RouteOne - */ - public function setEvent($event): RouteOne - { - $this->event = $event; - return $this; - } - - /** - * - * - * @return string|null - */ - public function getIdparent(): ?string - { - return $this->idparent; - } - - /** - * @param $idParent - * - * @return RouteOne - */ - public function setIdParent($idParent): RouteOne - { - $this->idparent = $idParent; - return $this; - } - - /** - * - * - * @return string - */ - public function getExtra(): string - { - return $this->extra; - } - - /** - * @param string $extra - * - * @return RouteOne - */ - public function setExtra(string $extra): RouteOne - { - $this->extra = $extra; - return $this; - } - - /** - * It gets the current category - * - * @return string|null - */ - public function getCategory(): ?string - { - return $this->category; - } - - /** - * It sets the current category - * - * @param string $category - * - * @return RouteOne - */ - public function setCategory(string $category): RouteOne - { - if (is_array($this->whitelist['category'])) { // there is a whitelist - if (in_array(strtolower($category), $this->whitelistLower['category'], true)) { - $p = array_search($category, $this->whitelistLower['category'], true); - $this->category = $this->whitelist['category'][$p]; // we returned the same value but with the right case. - return $this; - } - // and this value is not found there. - $this->category = $this->defCategory; - $this->notAllowed = true; - return $this; - } - $this->category = $category; - return $this; - } - - /** - * It gets the current sub category - * - * @return string - */ - public function getSubcategory(): string - { - return $this->subcategory; - } - - /** - * It gets the current sub-sub-category - * - * @return string - */ - public function getSubsubcategory(): string - { - return $this->subsubcategory; - } - - /** - * Returns true if the current web method is POST. - * - * @return bool - */ - public function isPostBack(): bool - { - return $this->isPostBack; - } - - /** - * It sets if the current state is postback - * - * @param bool $isPostBack - * - * @return RouteOne - */ - public function setIsPostBack(bool $isPostBack): RouteOne - { - $this->isPostBack = $isPostBack; - return $this; - } - - /** - * It gets the current list of module lists or null if there is none. - * - * @return array|bool - * @noinspection PhpUnused - */ - public function getModuleList() - { - return $this->moduleList; - } - - /** - * It sets the current list of modules or null to assigns nothing. - * - * @param array|bool $moduleList - * @noinspection PhpUnused - * - * @return RouteOne - */ - public function setModuleList($moduleList): RouteOne - { - $this->moduleList = $moduleList; - return $this; - } - - /** - * It gets the current strategy of module. - * - * @return string=['none','modulefront','nomodulefront'][$i] - * @see RouteOne::setModuleStrategy - */ - public function getModuleStrategy(): string - { - return $this->moduleStrategy; - } - - /** - * it changes the strategy to determine the type of url determined if the path has a module or not.
- * $forcedType must be null, otherwise this value is not used.
- * - * @param string $moduleStrategy - * - * @return RouteOne - */ - public function setModuleStrategy(string $moduleStrategy): RouteOne - { - $this->moduleStrategy = $moduleStrategy; - return $this; - } - - /** - * @return mixed|null - */ - private function getUrlFetchedOriginal() - { - $this->notAllowed = false; // reset - $this->isFetched = true; - $urlFetchedOriginal = $_GET[$this->argumentName] ?? null; // controller/action/id/.. - if ($urlFetchedOriginal !== null) { - $urlFetchedOriginal = rtrim($urlFetchedOriginal, '/'); - } - unset($_GET[$this->argumentName]); - /** @noinspection HostnameSubstitutionInspection */ - $this->httpHost = isset($_SERVER['HTTP_HOST']) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : ''; - $this->requestUri = isset($_SERVER['REQUEST_URI']) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : ''; - return $urlFetchedOriginal; - } - - /** - * @param string $urlFetched - * @param bool $sanitize - * @return array - */ - private function getExtracted(string $urlFetched, bool $sanitize = false): array - { - if ($sanitize) { - $urlFetched = filter_var($urlFetched, FILTER_SANITIZE_URL); - } - if (is_array($this->identify) && $this->type === '') { - foreach ($this->identify as $ty => $path) { - if ($path === '') { - $this->type = $ty; - break; - } - if (strpos($urlFetched ?? '', $path) === 0) { - $urlFetched = ltrim($this->str_replace_ex($path, '', $urlFetched, 1), '/'); - $this->type = $ty; - break; - } - } - } - return explode('/', $urlFetched); - } - - public function redirect(string $url, int $statusCode = 303): void - { - header('Location: ' . $url, true, $statusCode); - if (http_response_code()) { - die(1); - } - } - // - - /** - * @param string $key the name of the flag to read - * @param string|null $default is the default value is the parameter is set - * without value. - * @param bool $set it is the value returned when the argument is set but there is no value assigned - * @return string - */ - public static function getParameterCli(string $key, ?string $default = '', bool $set = true) - { - global $argv; - $p = array_search('-' . $key, $argv, true); - if ($p === false) { - return $default; - } - if (isset($argv[$p + 1])) { - return self::removeTrailSlash($argv[$p + 1]); - } - return $set; - } - - public static function isAbsolutePath($path): bool - { - if (!$path) { - return true; - } - if (DIRECTORY_SEPARATOR === '/') { - // linux and macos - return $path[0] === '/'; - } - return $path[1] === ':'; - } - - protected static function removeTrailSlash($txt): string - { - return rtrim($txt, '/\\'); - } - // -} + 'api', 'ws' => 'ws', 'controller' => '']; + /** @var array[] it holds the whitelist. Ex: ['controller'=>['a1','a2','a3']] */ + protected $whitelist = [ + 'controller' => null, + 'category' => null, + 'action' => null, + 'subcategory' => null, + 'subsubcategory' => null, + 'module' => null + ]; + protected $whitelistLower = [ + 'controller' => null, + 'category' => null, + 'action' => null, + 'subcategory' => null, + 'subsubcategory' => null, + 'module' => null + ]; + /** + * @var string|null=['api','ws','controller','front'][$i] + */ + private $forceType; + private $defController; + private $defAction; + private $defCategory; + private $defSubCategory; + private $defSubSubCategory; + private $defModule; + /** @var array|bool it stores the list of path used for the modules */ + private $moduleList; + private $isModule; + /** + * + * @var string=['none','modulefront','nomodulefront'][$i] + */ + private $moduleStrategy; + private $isFetched = false; + + /** + * RouteOne constructor. + * + * @param string $base base url with or without trailing slash (it's removed if its set).
+ * Example: ".","http://domain.dom", "http://domain.dom/subdomain"
+ * @param string|null $forcedType =['api','ws','controller','front'][$i]
+ * api then it expects a path as api/controller/action/id/idparent
+ * ws then it expects a path as ws/controller/action/id/idparent
+ * controller then it expects a path as controller/action/id/idparent
+ * front then it expects a path as /category/subc/subsubc/id
+ * @param bool|array $isModule if true then the route start reading a module name
+ * false controller/action/id/idparent
+ * true module/controller/action/id/idparent
+ * array if the value is an array then the value is determined if the + * first + * part of the path is in the array. Example + * ['modulefolder1','modulefolder2']
+ * @param bool $fetchValues (default false), if true then it also calls the method fetch() + * @param string $moduleStrategy =['none','modulefront','nomodulefront'][$i]
+ * it changes the strategy to determine the type of url determined if the path + * has a module or not.
+ * $forcedType must be null, otherwise this value is not calculated.
+ * + */ + public function __construct(string $base = '', ?string $forcedType = null, $isModule = false, + bool $fetchValues = false, string $moduleStrategy = 'none') + { + $this->base = rtrim($base, '/'); + $this->forceType = $forcedType; + $this->moduleStrategy = $moduleStrategy; + if ($forcedType !== null) { + $this->type = $forcedType; + } + if (is_bool($isModule)) { + $this->moduleList = null; + $this->isModule = $isModule; + } else { + $this->moduleList = $isModule; + $this->isModule = false; + } + $this->isPostBack = false; + if (isset($_SERVER['REQUEST_METHOD']) && in_array($_SERVER['REQUEST_METHOD'], $this->allowedVerbs, true)) { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $this->isPostBack = true; + } + $this->verb = $_SERVER['REQUEST_METHOD']; + } else { + $this->verb = 'GET'; + } + $this->setDefaultValues(); + if ($fetchValues) { + $this->fetch(); + } + } + + /** + * It gets the current instance of the library.
+ * If the instance does not exist, then it is created
+ * See constructor for the definition of the arguments. + * @param string $base + * @param string|null $forcedType + * @param bool|array $isModule + * @param bool $fetchValues + * @param string $moduleStrategy + * @return RouteOne + */ + public static function instance(string $base = '', ?string $forcedType = null, $isModule = false, + bool $fetchValues = false, string $moduleStrategy = 'none'): RouteOne + { + if (self::$instance === null) { + self::$instance = new RouteOne($base, $forcedType, $isModule, $fetchValues, $moduleStrategy); + } + return self::$instance; + } + + /** + * Returns true if there is an instance of CliOne. + * @return bool + */ + public static function hasInstance(): bool + { + return self::$instance !== null; + } + + /** + * It sets the default controller and action (if they are not entered in the route)
+ * It is uses to set a default route if the value is empty or its missing.
+ * Note:It must be set before fetch(). + * + * @param string $defController Default Controller + * @param string $defAction Default action + * @param string $defCategory Default category + * @param string $defSubCategory Default subcategory + * @param string $defSubSubCategory The default sub-sub-category + * @param string $defModule The default module. + * @return $this + */ + public function setDefaultValues(string $defController = '', string $defAction = '', string $defCategory = '', + string $defSubCategory = '', string $defSubSubCategory = '', + string $defModule = ''): self + { + if ($this->isFetched) { + throw new RuntimeException("RouteOne: you can't call setDefaultValues() after fetch()"); + } + $this->defController = $defController; + $this->defAction = $defAction; + $this->defCategory = $defCategory; + $this->defSubCategory = $defSubCategory; + $this->defSubSubCategory = $defSubSubCategory; + $this->defModule = $defModule; + return $this; + } + + /** + * It clears all the paths defined + * + * @return void + */ + public function clearPath(): void + { + $this->path = []; + $this->pathName = []; + $this->middleWare = []; + } + /** + * It adds a paths that could be evaluated using fetchPath()
+ * Example:
+ * ```php + * $this->addPath('api/{controller}/{action}/{id:0}','apipath'); + * $this->addPath('/api/{controller}/{action}/{id:0}/','apipath'); // "/" at the beginner and end are trimmed. + * $this->addPath('{controller}/{action}/{id:0}','webpath'); + * $this->addPath('{controller:root}/{action}/{id:0}','webpath'); // root path using default + * $this->addPath('somepath','namepath', + * function(callable $next,$id=null,$idparent=null,$event=null) { + * echo "middleware\n"; + * $result=$next($id,$idparent,$event); // calling the controller + * echo "endmiddleware\n"; + * return $result; + * }); + * ``` + * Note:
+ * The first part of the path, before the "{" is used to determine which path will be used.
+ * Example "path/{controller}" and "path/{controller}/{id}", the system will consider that are the same path + * @param string $path The path, example "aaa/{controller}/{action:default}/{id}"
+ * Where default is the optional default value. + * + * @param string|null $name (optional), the name of the path + * @param callable|null $middleWare A callable function used for middleware.
+ * The first argument of the function must be a callable method
+ * The next arguments must be the arguments defined by callObjectEx + * (id,idparent,event) + * @return $this + */ + public function addPath(string $path, ?string $name = null, ?callable $middleWare = null): RouteOne + { + if (!$path) { + throw new RuntimeException('Path must not be empty, use default value to set a root path'); + } + $path = trim($path, '/'); + $x0 = strpos($path, '{'); + if ($x0 === false) { + $partStr = ''; + $base = $path; + } else { + $base = substr($path, 0, $x0); + $partStr = substr($path, $x0); + } + $items = explode('/', $partStr); + $itemArr = []; + foreach ($items as $v) { + $p = trim($v, '{}' . " \t\n\r\0\x0B"); + $itemAdd = explode(':', $p, 2); + if (count($itemAdd) === 1) { + $itemAdd[] = null; // add a default value + } + $itemArr[] = $itemAdd; + } + if ($name === null) { + $this->pathName[] = $base; + $this->path[] = $itemArr; + $this->middleWare[] = $middleWare; + } else { + $this->pathName[$name] = $base; + $this->path[$name] = $itemArr; + $this->middleWare[$name] = $middleWare; + } + return $this; + } + + /** + * It fetches the path previously defined by addPath. + * + * @return int|string|null return null if not path is evaluated,
+ * otherwise, it returns the number/name of the path. It could return the value 0 (first path) + * @noinspection NotOptimalRegularExpressionsInspection + */ + public function fetchPath() + { + $this->lastError = []; + $this->currentPath = null; + $urlFetchedOriginal = $this->getUrlFetchedOriginal(); + $this->queries = $_GET; + unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); + foreach ($this->path as $pnum => $pattern) { + if ($this->pathName[$pnum] !== '' && strpos($urlFetchedOriginal ?? '', $this->pathName[$pnum]) !== 0) { + // basePath url does not match. + $this->lastError[$pnum] = "Pattern [$pnum], base url does not match"; + continue; + } + $urlFetched = substr($urlFetchedOriginal ?? '', strlen($this->pathName[$pnum])); + // nginx returns a path as /aaa/bbb apache aaa/bbb + if ($urlFetched !== '') { + $urlFetched = ltrim($urlFetched, '/'); + } + $path = $this->getExtracted($urlFetched); + foreach ($this->path[$pnum] as $key => $v) { + if ($v[1] === null) { + if (!array_key_exists($key, $path) || !isset($path[$key])) { + // the field is required but there we don't find any value + $this->lastError[$pnum] = "Pattern [$pnum] required field ($v[0]) not found in url"; + continue; + } + $name = $v[0]; + $value = $path[$key]; + } else { + $name = $v[0]; + if (isset($path[$key]) && $path[$key]) { + $value = $path[$key]; + } else { + // value not found, set default value + $value = $v[1]; + } + } + // 'controller', 'action', 'verb', 'event', 'type', 'module', 'id', 'idparent', 'category' + // , 'subcategory', 'subsubcategory' + switch ($name) { + case 'controller': + $this->controller = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case 'action': + $this->action = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case 'module': + $this->module = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case 'id': + $this->id = $value; + break; + case 'idparent': + $this->idparent = $value; + break; + case 'category': + $this->category = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case 'subcategory': + $this->subcategory = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case 'subsubcategory': + $this->subsubcategory = preg_replace('/[^a-zA-Z0-9_]/', "", $value); + break; + case '': + break; + default: + throw new RuntimeException("pattern incorrect [$name:$value]"); + } + } + $this->event = $this->getRequest('_event'); + $this->extra = $this->getRequest('_extra'); + $this->currentPath = $pnum; + break; + } + return $this->currentPath; + } + + /** + * + * It uses the next strategy to obtain the parameters;
+ * + */ + public function fetch(): void + { + //$urlFetched = $_GET['req'] ?? null; // controller/action/id/.. + $urlFetched = $this->getUrlFetchedOriginal(); // // controller/action/id/.. + $this->isFetched = true; + unset($_GET[$this->argumentName]); + /** @noinspection HostnameSubstitutionInspection */ + $this->httpHost = $_SERVER['HTTP_HOST'] ?? ''; + $this->requestUri = $_SERVER['REQUEST_URI'] ?? ''; + // nginx returns a path as /aaa/bbb apache aaa/bbb + if ($urlFetched !== '') { + $urlFetched = ltrim($urlFetched??'', '/'); + } + $this->queries = $_GET; + unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); + $path = $this->getExtracted($urlFetched, true); + //$first = $path[0] ?? $this->defController; + if (isset($path[0]) && $this->moduleList !== null) { + // if moduleArray has values then we find if the current path is a module or not. + $this->isModule = in_array($path[0], $this->moduleList, true); + } + $id = 0; + if ($this->isModule) { + $this->module = $path[$id] ?? $this->defModule; + $id++; + if ($this->moduleStrategy === 'modulefront') { + // the path is not a module, then type is set as front. + $this->type = 'front'; + } + } else { + $this->module = $this->defModule; + if ($this->moduleStrategy === 'nomodulefront') { + // the path is not a module, then type is set as front. + $this->type = 'front'; + } + } + if ($this->forceType !== null) { + $this->type = $this->forceType; + } + if (!$this->type) { + $this->type = 'controller'; + $this->setController((!$path[$id]) ? $this->defController : $path[$id]); + $id++; + } + switch ($this->type) { + case 'ws': + case 'api': + //$id++; [fixed] + $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); + $id++; + break; + case 'controller': + $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); + $id++; + break; + case 'front': + // it is processed differently. + $this->setCategory(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defCategory); + $id++; + $this->subcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubCategory; + $id++; + $this->subsubcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubSubCategory; + /** @noinspection PhpUnusedLocalVariableInspection */ + $id++; + $this->id = end($path); // id is the last element of the path + $this->event = $this->getRequest('_event'); + $this->extra = $this->getRequest('_extra'); + return; + } + $this->action = $path[$id] ?? null; + $id++; + $this->action = $this->action ?: $this->defAction; // $this->action is never undefined, so we don't need isset + $this->id = $path[$id] ?? null; + $id++; + $this->idparent = $path[$id] ?? null; + /** @noinspection PhpUnusedLocalVariableInspection */ + $id++; + $this->event = $this->getRequest('_event'); + $this->extra = $this->getRequest('_extra'); + } + + /** + * @param string $search + * @param array|string $replace + * @param string $subject + * @param int $limit + * @return string + */ + protected function str_replace_ex(string $search, $replace, string $subject, int $limit = 99999): string + { + return implode($replace, explode($search, $subject, $limit + 1)); + } + + /** + * It is an associative array with the allowed paths or null (default behaviour) to allows any path.
+ * The comparison ignores cases but the usage is "case-sensitive" and it uses the case used here
+ * For example: if we allowed the controller called "Controller1" then:
+ * + * Example: + * ```php + * // we only want to allow the controllers called Purchase, Invoice and Customer. + * $this->setWhiteList('controller',['Purchase','Invoice','Customer']); + * ``` + * Note: this must be executed before fetch() + * @param string $type =['controller','category','action','subcategory','subsubcategory','module'][$i] + * @param array|null $array if null (default value) then we don't validate the information. + */ + public function setWhiteList(string $type, ?array $array): void + { + if ($this->isFetched && $array !== null) { + throw new RuntimeException("RouteOne: you can't call setWhiteList() after fetch()"); + } + $type = strtolower($type); + $this->whitelist[$type] = $array; + $this->whitelistLower[$type] = is_array($array) ? array_map('strtolower', $array) : null; + } + + /** + * If the subdomain is empty or different to www, then it redirects to www.domain.com.
+ * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
+ * Note: If this code needs to redirect, then it stops the execution of the code. Usually, + * it must be called at the top of the code + * + * @param bool $https If true then it also redirects to https + * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new full url + * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
+ * Otherwise, if $redirect=false, it returns the full url to redirect. + */ + public function alwaysWWW(bool $https = false, bool $redirect = true): ?string + { + $url = $this->httpHost; + //if (strpos($url, '.') === false || ip2long($url)) { + //} + if (strpos($url ?? '', 'www.') === false) { + $location = $this->getLocation($https); + $location .= '//www.' . $url . $this->requestUri; + if ($redirect) { + header('HTTP/1.1 301 Moved Permanently'); + header('Location: ' . $location); + if (http_response_code()) { + die(1); + } + } + return $location; + } + if ($https) { + return $this->alwaysHTTPS(false); + } + return null; + } + + private function getLocation($https): string + { + if ($https) { + $port = $_SERVER['HTTP_PORT'] ?? '443'; + $location = 'https:'; + if ($port !== '443' && $port !== '80') { + $location .= $port; + } + } else { + $port = $_SERVER['HTTP_PORT'] ?? '80'; + $location = 'http:'; + if ($port !== '80') { + $location .= $port; + } + } + return $location; + } + + /** + * If the page is loaded as http, then it redirects to https
+ * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
+ * Note: If this code needs to redirect, then it stops the execution of the code. Usually, + * it must be called at the top of the code + * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url + * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
+ * Otherwise, if $redirect=false, it returns the url to redirect. + */ + public function alwaysHTTPS(bool $redirect = true): ?string + { + if (strpos($this->httpHost, '.') === false || ip2long($this->httpHost)) { + return null; + } + $https = $_SERVER['HTTPS'] ?? ''; + if (empty($https) || $https === 'off') { + $port = $_SERVER['HTTP_PORT'] ?? '443'; + $port = ($port === '443' || $port === '80') ? '' : $port; + $location = 'https:' . $port . '//' . $this->httpHost . $this->requestUri; + if ($redirect) { + header('HTTP/1.1 301 Moved Permanently'); + header('Location: ' . $location); + if (http_response_code()) { + die(1); + } + } + return $location; + } + return null; + } + + /** + * If the subdomain is www (example www.domain.dom) then it redirects to a naked domain "domain.dom"
+ * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
+ * Note: If this code needs to redirect, then we should stop the execution of any other code. Usually, + * it must be called at the top of the code + * + * @param bool $https If true then it also redirects to https + * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url + * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
+ * Otherwise, if $redirect=false, it returns the full url to redirect. + */ + public function alwaysNakedDomain(bool $https = false, bool $redirect = true): ?string + { + $url = $this->httpHost; + if (strpos($url ?? '', 'www.') === 0) { + $host = substr($url, 4); // we remove the www. at first + $location = $this->getLocation($https); + $location .= '//' . $host . $this->requestUri; + if ($redirect) { + header('HTTP/1.1 301 Moved Permanently'); + header('Location: ' . $location); + if (http_response_code()) { + die(1); + } + return ''; + } + return $location; + } + if ($https) { + return $this->alwaysHTTPS(false); + } + return null; + } + + /** + * It creates and object (for example, a Controller object) and calls the method.
+ * Example: (type controller,api,ws) + * ```php + * $this->callObject('cocacola\controller\%sController'); // %s is replaced by the name of the current controller + * $this->callObject('namespace/%2s/%1sClass'); // it calls namespace/Module/ExampleClass (only if module is able) + * $this->callObject('namespace/%2s/%3s%/%1sClass'); // %3s is for the type of path + * ``` + * Note: The method called should be written as (static or not)
+ * ```php + * public function *nameaction*Action($id="",$idparent="",$event="") { } + * ``` + * + * @param string $classStructure structure of the class.
+ * Type=controller,api,ws
+ * The first %s (or %1s) is the name of the controller.
+ * The second %s (or %2s) is the name of the module (if any and if + * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. + * controller,api,ws,front)
+ * Type=front
+ * The first %s (or %1s) is the name of the category.
+ * The second %s (or %2s) is the name of the subcategory
+ * The third %s (or %3s) is the type of the subsubcategory
+ * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If false then it + * returns the error (if any) + * @param string $method [optional] Default value='%sAction'. The name of the method to call (get/post). + * If method does not exist then it will use $methodGet or $methodPost + * @param string $methodGet [optional] Default value='%sAction'. The name of the method to call (get) but only + * if the method defined by $method is not defined. + * @param string $methodPost [optional] Default value='%sAction'. The name of the method to call (post) but + * only + * if the method defined by $method is not defined. + * @param array $arguments [optional] Default value=['id','idparent','event'] the arguments to pass to the + * function + * + * @return string|null null if the operation was correct, or the message of error if it failed. + * @throws Exception + * @deprecated + * @see self::callObjectEx Use callObjectEx('{controller}Controller'); instead of callObject('%sController); + */ + public function callObject( + string $classStructure = '%sController', bool $throwOnError = true, + string $method = '%sAction', string $methodGet = '%sActionGet', + string $methodPost = '%sActionPost', + array $arguments = ['id', 'idparent', 'event'] + ): ?string + { + if ($this->notAllowed === true) { + throw new UnexpectedValueException('Input is not allowed'); + } + if ($this->type !== 'front') { + if ($this->controller === null) { + throw new UnexpectedValueException('Controller is not set or it is not allowed'); + } + $op = sprintf($classStructure, $this->controller, $this->module, $this->type); + } else { + $op = sprintf($classStructure, $this->category, $this->subcategory, $this->subsubcategory); + } + if (!class_exists($op)) { + if ($throwOnError) { + throw new RuntimeException("Class $op doesn't exist"); + } + return "Class $op doesn't exist"; + } + try { + $controller = new $op(); + if ($this->type !== 'front') { + $actionRequest = sprintf($method, $this->action); + } else { + /** @noinspection PrintfScanfArgumentsInspection */ + $actionRequest = sprintf($method, $this->subcategory, $this->subsubcategory); + } + $actionGetPost = (!$this->isPostBack) ? sprintf($methodGet, $this->action) + : sprintf($methodPost, $this->action); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + $args = []; + foreach ($arguments as $a) { + $args[] = $this->{$a}; + } + if (method_exists($controller, $actionRequest)) { + try { + $controller->{$actionRequest}(...$args); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + } elseif (method_exists($controller, $actionGetPost)) { + try { + $controller->{$actionGetPost}(...$args); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + } else { + $pb = $this->isPostBack ? '(POST)' : '(GET)'; + $msgError = "Action [$actionRequest or $actionGetPost] $pb not found for class [$op]"; + $msgError = strip_tags($msgError); + if ($throwOnError) { + throw new UnexpectedValueException($msgError); + } + return $msgError; + } + return null; + } + + /** + * Get multiples values, get, post, request, header, etc. + * @param string $key The name of the key to read.
+ * Body and verb do not use a key. + * @param string $type =['get','post','request','header','body','verb'][$i] + * @param mixed|null $defaultValue the default value if the value is not found.
+ * It is ignored by body and verb because both always returns a value + * @return false|mixed|string|null + * @throws RuntimeException + */ + public function getMultiple(string $key, string $type, $defaultValue = null) + { + switch ($type) { + case 'get': + $r = $this->getQuery($key, $defaultValue); + break; + case 'post': + $r = $this->getPost($key, $defaultValue); + break; + case 'request': + $r = $this->getRequest($key, $defaultValue); + break; + case 'header': + $r = $this->getHeader($key, $defaultValue); + break; + case 'body': + $r = $this->getBody(true); + $r = $r === false ? $defaultValue : $r; + break; + case 'verb': + $r = $this->verb; + break; + default: + throw new RuntimeException("argument incorrect, type [$type] unknown"); + } + return $r; + } + + /** + * It creates and object (for example, a Controller object) and calls the method.
+ * Note: It is an advanced version of this::callObject()
+ * This method uses {} to replace values.
+ * + * Note: You can also convert the case + * + * Example:
+ * ```php + * // controller example http://somedomain/Customer/Insert/23 + * $this->callObjectEx('cola\controller\{controller}Controller'); + * // it calls the method cola\controller\Customer::InsertAction(23,'',''); + * + * $this->callObjectEx('cola\controller\{controller}Controller','{action}Action{verb}'); + * // it calls the method cola\controller\Customer::InsertActionGet(23,'',''); or InsertActionPost, etc. + * + * // front example: http://somedomain/product/coffee/nescafe/1 + * $this->callObjectEx('cocacola\controller\{category}Controller',false,'{subcategory}',null + * ,null,['subsubcategory','id']); + * // it calls the method cocacola\controller\product::coffee('nescafe','1'); + * + * // callable instead of a class + * $this->callObjectEx(function($id,$idparent,$event) { echo "hi"; }); + * ``` + * + * @param string|object|callable $classStructure [optional] Default value='{controller}Controller'.
+ * If classStructure is an string then it must indicate the + * full name of the class including namespaces + * (SomeClassController::class is allowed)
+ * if $classStructure is an object, + * then it uses the instance of it
+ * if $classStructure is a callable, then it calls the + * function. The arguments are defined by $arguments
+ * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If + * false then it returns the error as a string (if any) + * @param string|null $method [optional] Default value='{action}Action'. The name of the method + * to call + * (get/post). If the method does not exist then it will use + * $methodGet + * (isPostBack=false) or $methodPost (isPostBack=true) + * @param string|null $methodGet [optional] Default value='{action}Action{verb}'. The name of the + * method to call when get + * (get) but only if the method defined by $method is not defined. + * @param string|null $methodPost [optional] Default value='{action}Action{verb}'. The name of the + * method to call + * (post) but only if the method defined by $method is not defined. + * @param array $arguments [optional] Default value=['id','idparent','event']
+ * Values allowed:'get','post','request','header','body','verb'
+ * T The arguments to + * pass to the methods and middleware
+ * Example
+ * + * @param array $injectArguments [optional] You can inject values into the argument of the + * instance's constructor.
It will do nothing if you pass an + * object as + * $classStructure. + * @param string $onlyPath default is "*"(any path), if set, then this method will only work + * if the path + * (obtained by fetchPath) is the indicated here. + * @return string|null Returns a string with an error or null if not error. + * If $classStructure is callable, then it returns the value of the + * function. + * @throws Exception + */ + public function callObjectEx( + $classStructure = '{controller}Controller', bool $throwOnError = true, + ?string $method = '{action}Action', ?string $methodGet = '{action}Action{verb}', + ?string $methodPost = '{action}Action{verb}', array $arguments = ['id', 'idparent', 'event'], + array $injectArguments = [], + string $onlyPath = '*' + ): ?string + { + if ($onlyPath !== '*' && $this->currentPath !== $onlyPath) { + // This object must be called using a specific path. + return null; + } + if ($this->notAllowed === true) { + throw new UnexpectedValueException('Input method is not allowed', 403); + } + if (is_object($classStructure)) { + $className = get_class($classStructure); + } else if (is_callable($classStructure)) { + $className = '**CALLABLE**'; + } else { + $className = $this->replaceNamed($classStructure); + } + if (!class_exists($className) && $className !== '**CALLABLE**') { + if ($throwOnError) { + throw new RuntimeException("Class $className doesn't exist", 404); + } + return "Class $className doesn't exist"; + } + $args = []; + foreach ($arguments as $keyArg => $valueArg) { + if (in_array($valueArg, $this->allowedFields, true)) { + $args[$keyArg] = $this->{$valueArg}; + } else if (is_string($valueArg) || is_numeric($valueArg)) { + $x = explode(':', $valueArg, 3); // get:fieldname:defaultvalue + if (count($x) < 2) { + $msg = 'RouteOne::callObjectEx, argument incorrect, use type:name:default or a defined name'; + if ($throwOnError) { + throw new RuntimeException($msg); + } + return $msg; + } + try { + $args[$keyArg] = $this->getMultiple($x[1], $x[0], $x[2] ?? null); + } catch (Exception $ex) { + if ($throwOnError) { + throw new RuntimeException($ex->getMessage()); + } + return $ex->getMessage(); + } + } else { + // ['field'=>$someobjectorarray] or [$someobjectorarray] + $args[$keyArg] = $valueArg; + } + } + try { + if (is_callable($classStructure)) { + if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { + return $this->middleWare[$this->currentPath]($classStructure, ...$args); + } + return $classStructure(...$args); + } + if (is_object($classStructure)) { + $controller = $classStructure; + } else if(method_exists($className,'getInstance')) { + $controller=$className->getInstance(); // try to autowire an instance. + } elseif(method_exists($className,'instance')) { + $controller=$className->instance(); // try to autowire an instance + } else { + $controller = new $className(...$injectArguments); // try to create a new controller. + } + $actionRequest = $this->replaceNamed($method); + $actionGetPost = (!$this->isPostBack) ? $this->replaceNamed($methodGet) + : $this->replaceNamed($methodPost); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + if (method_exists($controller, $actionRequest)) { + /** @noinspection DuplicatedCode */ + try { + //$call = $controller->{$actionRequest}; + if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { + return $this->middleWare[$this->currentPath]( + static function(...$args) use ($controller,$actionRequest) { // it is a wrapper function + return $controller->{$actionRequest}(...$args); + } + , ...$args); + } + $controller->{$actionRequest}(...$args); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + } elseif (method_exists($controller, $actionGetPost)) { + /** @noinspection DuplicatedCode */ + try { + if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { + //return $this->middleWare[$this->currentPath]($call, ...$args); + return $this->middleWare[$this->currentPath]( + static function(...$args) use ($controller,$actionGetPost) { // it is a wrapper function + return $controller->{$actionGetPost}(...$args); + } + , ...$args); + } + $controller->{$actionGetPost}(...$args); + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + } else { + $pb = $this->isPostBack ? '(POST)' : '(GET)'; + $msgError = "Action ex [$actionRequest or $actionGetPost] $pb not found for class [$className]"; + $msgError = strip_tags($msgError); + if ($throwOnError) { + throw new UnexpectedValueException($msgError, 400); + } + return $msgError; + } + return null; + } + + /** + * Return a formatted string like vsprintf() with named placeholders.
+ * When a placeholder doesn't have a matching key (it's not in the whitelist $allowedFields), then the value + * is not modified, and it is returned as is.
+ * If the name starts with uc_,lc_,u_,l_ then it is converted into ucfirst,lcfirst,uppercase or lowercase. + * + * @param string|null $format + * + * @return string + */ + private function replaceNamed(?string $format): string + { + if ($format === null) { + return ''; + } + return preg_replace_callback("/{(\w+)}/", function($matches) { + $nameField = $matches[1]; + $result = ''; + if (strpos($nameField ?? '', '_') > 0) { + [$x, $nf] = explode('_', $nameField, 2); + if (in_array($nf, $this->allowedFields, true) === false) { + return '{' . $nameField . '}'; + } + switch ($x) { + case 'uc': + $result = ucfirst(strtolower($this->{$nf})); + break; + case 'lc': + $result = lcfirst(strtoupper($this->{$nf})); + break; + case 'u': + $result = strtoupper($this->{$nf}); + break; + case 'l': + $result = strtolower($this->{$nf}); + break; + } + } else { + if (in_array($nameField, $this->allowedFields, true) === false) { + return '{' . $nameField . '}'; + } + $result = $this->{$nameField}; + } + return $result; + }, $format); + } + + /** + * It calls (include) a file using the current controller. + * + * @param string $fileStructure It uses sprintf
+ * The first %s (or %1s) is the name of the controller.
+ * The second %s (or %2s) is the name of the module (if any and if + * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. + * controller,api,ws,front)
Example %s.php => controllername.php
Example + * %s3s%/%1s.php => controller/controllername.php + * @param bool $throwOnError + * + * @return string|null + * @throws Exception + */ + public function callFile(string $fileStructure = '%s.php', bool $throwOnError = true): ?string + { + $op = sprintf($fileStructure, $this->controller, $this->module, $this->type); + try { + include $op; + } catch (Exception $ex) { + if ($throwOnError) { + throw $ex; + } + return $ex->getMessage(); + } + return null; + } + + /** + * Returns the current base url without traling space, paremters or queries/b
+ * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and + * it could be modified by the user. + * + * @param bool $withoutFilename if true then it doesn't include the filename + * + * @return string + */ + public function getCurrentUrl(bool $withoutFilename = true): string + { + $sn = $_SERVER['SCRIPT_NAME'] ?? ''; + if ($withoutFilename) { + return dirname($this->getCurrentServer() . $sn); + } + return $this->getCurrentServer() . $sn; + } + + /** + * It returns the current server without trailing slash.
+ * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and + * it could be modified by the user. + * + * @return string + */ + public function getCurrentServer(): string + { + $server_name = $this->serverName ?? $_SERVER['SERVER_NAME'] ?? null; + $c = filter_var($server_name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); + $server_name = $c ? $server_name : $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; + $sp = $_SERVER['SERVER_PORT'] ?? 80; + $port = !in_array($sp, ['80', '443'], true) ? ':' . $sp : ''; + $https = $_SERVER['HTTPS'] ?? ''; + $scheme = !empty($https) && (strtolower($https) === 'on' || $https === '1') ? 'https' : 'http'; + return $scheme . '://' . $server_name . $port; + } + + /** + * It sets the current server name. It is used by getCurrentUrl() and getCurrentServer() + * + * @param string $serverName Example: "localhost", "127.0.0.1", "www.site.com", etc. + * + * @see RouteOne::getCurrentUrl + * @see RouteOne::getCurrentServer + */ + public function setCurrentServer(string $serverName): void + { + $this->serverName = $serverName; + } + + /** + * It sets the values of the route using customer values
+ * If the values are null, then it keeps the current values (if any) + * + * @param null|string $module Name of the module + * @param null|string $controller Name of the controller. + * @param null|string $action Name of the action + * @param null|string $id Name of the id + * @param null|string $idparent Name of the idparent + * @param null|string $category Value of the category + * @param string|null $subcategory Value of the subcategory + * @param string|null $subsubcategory Value of the sub-subcategory + * @return $this + */ + public function url( + ?string $module = null, + ?string $controller = null, + ?string $action = null, + ?string $id = null, + ?string $idparent = null, + ?string $category = null, + ?string $subcategory = null, + ?string $subsubcategory = null + ): self + { + if ($module !== null) { + $this->module = $module; + } + if ($controller !== null) { + $this->setController($controller); + } + if ($action !== null) { + $this->action = $action; + } + if ($id !== null) { + $this->id = $id; + } + if ($idparent !== null) { + $this->idparent = $idparent; + } + if ($category !== null) { + $this->category = $category; + } + if ($subcategory !== null) { + $this->subcategory = $subcategory; + } + if ($subsubcategory !== null) { + $this->subsubcategory = $subsubcategory; + } + $this->extra = null; + $this->event = null; + return $this; + } + + public function urlFront( + $module = null, + $category = null, + $subcategory = null, + $subsubcategory = null, + $id = null + ): RouteOne + { + if ($module) { + $this->module = $module; + } + if ($category) { + $this->setCategory($category); + } + if ($subcategory) { + $this->subcategory = $subcategory; + } + if ($subsubcategory) { + $this->subsubcategory = $subsubcategory; + } + if ($id) { + $this->id = $id; + } + $this->extra = null; + $this->event = null; + return $this; + } + + // .htaccess: + // RewriteRule ^(.*)$ index.php?req=$1 [L,QSA] + public function reset(): RouteOne + { + // $this->base=''; base is always keep + $this->isFetched = false; + $this->defController = ''; + $this->defCategory = ''; + $this->defSubCategory = ''; + $this->defSubSubCategory = ''; + $this->defModule = ''; + $this->forceType = null; + $this->defAction = ''; + $this->isModule = ''; + $this->moduleStrategy = 'none'; + $this->moduleList = null; + $this->id = null; + $this->event = null; + $this->idparent = null; + $this->extra = null; + $this->verb = 'GET'; + $this->notAllowed = false; + $this->clearPath(); + $this->currentPath = null; + return $this; + } + + /** + * This function is used to identify the type automatically. If the url is empty then it is marked as default
+ * It returns the first one that matches. + * Example:
+ * ```php + * $this->setIdentifyType([ + * 'controller' =>'backend', // domain.dom/backend/controller/action => controller type + * 'api'=>'api', // domain.dom/api/controller => api type + * 'ws'=>'api/ws' // domain.dom/api/ws/controller => ws type + * 'front'=>'' // domain.dom/* =>front (any other that does not match) + * ]); + * ``` + * + * @param $array + */ + public function setIdentifyType($array): void + { + $this->identify = $array; + } + + /** + * It returns a non route url based in the base url.
+ * Example:
+ * $this->getNonRouteUrl('login.php'); // http://baseurl.com/login.php + * + * @param string $urlPart + * + * @return string + * @see RouteOne + */ + public function getNonRouteUrl(string $urlPart): string + { + return $this->base . '/' . $urlPart; + } + + /** + * It reconstructs an url using the current information.
+ * Example:
+ * ```php + * $currenturl=$this->getUrl(); + * $buildurl=$this->url('mod','controller','action',20)->getUrl(); + * ``` + * Note:. It discards any information outside the values pre-defined + * (example: /controller/action/id/idparent/?arg=1&arg=2)
+ * It does not consider the path() structure but the type of url. + * @param string $extraQuery If we want to add extra queries + * @param bool $includeQuery If true then it includes the queries in $this->queries + * + * @return string + */ + public function getUrl(string $extraQuery = '', bool $includeQuery = false): string + { + $url = $this->base . '/'; + if ($this->isModule) { + $url .= $this->module . '/'; + } + switch ($this->type) { + case 'ws': + case 'controller': + case 'api': + $url .= ''; + break; + case 'front': + $url .= "$this->category/$this->subcategory/$this->subsubcategory/"; + if ($this->id) { + $url .= $this->id . '/'; + } + if ($this->idparent) { + $url .= $this->idparent . '/'; + } + return $url; + default: + trigger_error('type [' . $this->type . '] not defined'); + break; + } + $url .= $this->controller . '/'; // Controller is always visible, even if it is empty + $url .= $this->action . '/'; // action is always visible, even if it is empty + $sepQuery = '?'; + if (($this->id !== null && $this->id !== '') || $this->idparent !== null) { + $url .= $this->id . '/'; // id is visible if id is not empty or if idparent is not empty. + } + if ($this->idparent !== null && $this->idparent !== '') { + $url .= $this->idparent . '/'; // idparent is only visible if it is not empty (zero is not empty) + } + if ($this->event !== null && $this->event !== '') { + $url .= '?_event=' . $this->event; + $sepQuery = '&'; + } + if ($this->extra !== null && $this->extra !== '') { + $url .= $sepQuery . '_extra=' . $this->extra; + $sepQuery = '&'; + } + if ($extraQuery !== null && $extraQuery !== '') { + $url .= $sepQuery . $extraQuery; + $sepQuery = '&'; + } + if ($includeQuery && count($this->queries)) { + $url .= $sepQuery . http_build_query($this->queries); + } + return $url; + } + + /** + * Returns the url using the path and current values
+ * The trail "/" is always removed. + * @param string|null $idPath If null then it uses the current path obtained by fetchUrl()
+ * If not null, then it uses the id path to obtain the path. + * @return string + */ + public function getUrlPath(?string $idPath = null): string + { + $idPath = $idPath ?? $this->currentPath; + if (!isset($this->path[$idPath])) { + throw new RuntimeException("Path $idPath not defined"); + } + $patternItems = $this->path[$idPath]; + $url = $this->base . '/' . $this->pathName[$idPath]; + $final = []; + foreach ($patternItems as $vArr) { + [$idx, $def] = $vArr; + $value = $this->{$idx} ?? $def; + $final[] = $value; + } + $url .= implode('/', $final); + return rtrim($url, '/'); + } + + /** + * It returns the current type. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * It returns the current name of the module + * + * @return string + */ + public function getModule(): string + { + return $this->module; + } + + /** + * @param string $key + * @param null|mixed $valueIfNotFound + * + * @return mixed + */ + public function getQuery(string $key, $valueIfNotFound = null) + { + return $this->queries[$key] ?? $valueIfNotFound; + } + + /** + * It gets the current header (if any) + * + * @param string $key The key to read + * @param null|mixed $valueIfNotFound + * @return mixed|null + */ + public function getHeader(string $key, $valueIfNotFound = null) + { + $keyname = 'HTTP_' . strtoupper($key); + return $_SERVER[$keyname] ?? $valueIfNotFound; + } + + /** + * It gets the Post value if not the Get value + * + * @param string $key The key to read + * @param null|mixed $valueIfNotFound + * @return mixed|null + */ + public function getRequest(string $key, $valueIfNotFound = null) + { + return $_POST[$key] ?? $_GET[$key] ?? $valueIfNotFound; + } + + /** + * It gets the Post value or returns the default value if not found + * + * @param string $key The key to read + * @param null|mixed $valueIfNotFound + * @return mixed|null + */ + public function getPost(string $key, $valueIfNotFound = null) + { + return $_POST[$key] ?? $valueIfNotFound; + } + + /** + * It gets the Get (url parameter) value or returns the default value if not found + * + * @param string $key The key to read + * @param null|mixed $valueIfNotFound + * @return mixed|null + */ + public function getGet(string $key, $valueIfNotFound = null) + { + return $_GET[$key] ?? $valueIfNotFound; + } + + /** + * It gets the body of a request. + * + * @param bool $jsonDeserialize if true then it de-serialize the values. + * @param bool $asAssociative if true (default value) then it returns as an associative array. + * @return false|mixed|string + */ + public function getBody(bool $jsonDeserialize = false, bool $asAssociative = true) + { + $entityBody = file_get_contents('php://input'); + if (!$jsonDeserialize) { + return $entityBody; + } + return json_decode($entityBody, $asAssociative); + } + + /** + * It sets a query value + * + * @param string $key + * @param null|mixed $value + */ + public function setQuery(string $key, $value): void + { + $this->queries[$key] = $value; + } + + /** + * It returns the current name of the controller. + * + * @return string|null + */ + public function getController(): ?string + { + return $this->controller; + } + + /** + * + * @param $controller + * + * @return RouteOne + */ + public function setController($controller): RouteOne + { + if (is_array($this->whitelist['controller'])) { // there is a whitelist + if (in_array(strtolower($controller), $this->whitelistLower['controller'], true)) { + $p = array_search($controller, $this->whitelistLower['controller'], true); + $this->controller = $this->whitelist['controller'][$p]; // we returned the same value but with the right case. + return $this; + } + // and this value is not found there. + $this->controller = $this->defController; + $this->notAllowed = true; + return $this; + } + $this->controller = $controller; + return $this; + } + + /** + * + * + * @return string|null + */ + public function getAction(): ?string + { + return $this->action; + } + + /** + * + * + * @param $action + * + * @return RouteOne + */ + public function setAction($action): RouteOne + { + $this->action = $action; + return $this; + } + + /** + * + * + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * + * + * @param $id + * + * @return RouteOne + */ + public function setId($id): RouteOne + { + $this->id = $id; + return $this; + } + + /** + * + * + * @return string + */ + public function getEvent(): string + { + return $this->event; + } + + /** + * + * + * @param $event + * + * @return RouteOne + */ + public function setEvent($event): RouteOne + { + $this->event = $event; + return $this; + } + + /** + * + * + * @return string|null + */ + public function getIdparent(): ?string + { + return $this->idparent; + } + + /** + * @param $idParent + * + * @return RouteOne + */ + public function setIdParent($idParent): RouteOne + { + $this->idparent = $idParent; + return $this; + } + + /** + * + * + * @return string + */ + public function getExtra(): string + { + return $this->extra; + } + + /** + * @param string $extra + * + * @return RouteOne + */ + public function setExtra(string $extra): RouteOne + { + $this->extra = $extra; + return $this; + } + + /** + * It gets the current category + * + * @return string|null + */ + public function getCategory(): ?string + { + return $this->category; + } + + /** + * It sets the current category + * + * @param string $category + * + * @return RouteOne + */ + public function setCategory(string $category): RouteOne + { + if (is_array($this->whitelist['category'])) { // there is a whitelist + if (in_array(strtolower($category), $this->whitelistLower['category'], true)) { + $p = array_search($category, $this->whitelistLower['category'], true); + $this->category = $this->whitelist['category'][$p]; // we returned the same value but with the right case. + return $this; + } + // and this value is not found there. + $this->category = $this->defCategory; + $this->notAllowed = true; + return $this; + } + $this->category = $category; + return $this; + } + + /** + * It gets the current sub category + * + * @return string + */ + public function getSubcategory(): string + { + return $this->subcategory; + } + + /** + * It gets the current sub-sub-category + * + * @return string + */ + public function getSubsubcategory(): string + { + return $this->subsubcategory; + } + + /** + * Returns true if the current web method is POST. + * + * @return bool + */ + public function isPostBack(): bool + { + return $this->isPostBack; + } + + /** + * It sets if the current state is postback + * + * @param bool $isPostBack + * + * @return RouteOne + */ + public function setIsPostBack(bool $isPostBack): RouteOne + { + $this->isPostBack = $isPostBack; + return $this; + } + + /** + * It gets the current list of module lists or null if there is none. + * + * @return array|bool + * @noinspection PhpUnused + */ + public function getModuleList() + { + return $this->moduleList; + } + + /** + * It sets the current list of modules or null to assigns nothing. + * + * @param array|bool $moduleList + * @noinspection PhpUnused + * + * @return RouteOne + */ + public function setModuleList($moduleList): RouteOne + { + $this->moduleList = $moduleList; + return $this; + } + + /** + * It gets the current strategy of module. + * + * @return string=['none','modulefront','nomodulefront'][$i] + * @see RouteOne::setModuleStrategy + */ + public function getModuleStrategy(): string + { + return $this->moduleStrategy; + } + + /** + * it changes the strategy to determine the type of url determined if the path has a module or not.
+ * $forcedType must be null, otherwise this value is not used.
+ *
    + *
  • none:if the path uses a module then the type is calculated normally (default)
  • + *
  • modulefront:if the path uses a module then the type is front. If it doesn't use a module + * then it is a controller, api or ws
  • + *
  • nomodulefront:if the path uses a module then the type is controller, api or ws. + * If it doesn't use module then it is front
  • + *
+ * @param string $moduleStrategy + * + * @return RouteOne + */ + public function setModuleStrategy(string $moduleStrategy): RouteOne + { + $this->moduleStrategy = $moduleStrategy; + return $this; + } + + /** + * @return mixed|null + */ + private function getUrlFetchedOriginal() + { + $this->notAllowed = false; // reset + $this->isFetched = true; + $urlFetchedOriginal = $_GET[$this->argumentName] ?? null; // controller/action/id/.. + if ($urlFetchedOriginal !== null) { + $urlFetchedOriginal = rtrim($urlFetchedOriginal, '/'); + } + unset($_GET[$this->argumentName]); + /** @noinspection HostnameSubstitutionInspection */ + $this->httpHost = isset($_SERVER['HTTP_HOST']) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : ''; + $this->requestUri = isset($_SERVER['REQUEST_URI']) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : ''; + return $urlFetchedOriginal; + } + + /** + * @param string $urlFetched + * @param bool $sanitize + * @return array + */ + private function getExtracted(string $urlFetched, bool $sanitize = false): array + { + if ($sanitize) { + $urlFetched = filter_var($urlFetched, FILTER_SANITIZE_URL); + } + if (is_array($this->identify) && $this->type === '') { + foreach ($this->identify as $ty => $path) { + if ($path === '') { + $this->type = $ty; + break; + } + if (strpos($urlFetched ?? '', $path) === 0) { + $urlFetched = ltrim($this->str_replace_ex($path, '', $urlFetched, 1), '/'); + $this->type = $ty; + break; + } + } + } + return explode('/', $urlFetched); + } + + public function redirect(string $url, int $statusCode = 303): void + { + header('Location: ' . $url, true, $statusCode); + if (http_response_code()) { + die(1); + } + } + // + + /** + * @param string $key the name of the flag to read + * @param string|null $default is the default value is the parameter is set + * without value. + * @param bool $set it is the value returned when the argument is set but there is no value assigned + * @return string + */ + public static function getParameterCli(string $key, ?string $default = '', bool $set = true) + { + global $argv; + $p = array_search('-' . $key, $argv, true); + if ($p === false) { + return $default; + } + if (isset($argv[$p + 1])) { + return self::removeTrailSlash($argv[$p + 1]); + } + return $set; + } + + public static function isAbsolutePath($path): bool + { + if (!$path) { + return true; + } + if (DIRECTORY_SEPARATOR === '/') { + // linux and macos + return $path[0] === '/'; + } + return $path[1] === ':'; + } + + protected static function removeTrailSlash($txt): string + { + return rtrim($txt, '/\\'); + } + // +} diff --git a/lib/RouteOneCli.php b/lib/RouteOneCli.php index 93c0232..bbd1685 100644 --- a/lib/RouteOneCli.php +++ b/lib/RouteOneCli.php @@ -1,441 +1,441 @@ -route = new RouteOne(); - $this->cli = CliOne::instance(); - if (!CliOne::hasMenu()) { - $this->cli->setErrorType(); - $this->cli->addMenu('mainmenu', - function($cli) { - $cli->upLevel('main menu'); - $cli->setColor(['byellow'])->showBread(); - } - , function(CliOne $cli) { - $cli->downLevel(2); - }); - } - $this->cli->addMenuService('mainmenu', $this); - $this->cli->addMenuItem('mainmenu', 'router', - '[{{routerconfigfull}}] Configure the router', 'navigate:routermenu'); - $this->cli->addMenu('routermenu', - function($cli) { - $cli->upLevel('router menu'); - $cli->setColor(['byellow'])->showBread(); - } - , 'footer'); - /** - * The next comments are used to indicate that the methods indicated are used here. - * @see RouteOneCli::menurouteroneconfigure - * @see RouteOneCli::menurouteronehtaccess - * @see RouteOneCli::menurouteronerouter - * @see RouteOneCli::menurouteronepaths - * @see RouteOneCli::menurouteroneload - * @see RouteOneCli::menurouteronesave - */ - $this->cli->addMenuItems('routermenu', [ - 'configure' => ['[{{routerconfig}}] configure and connect to the database', 'routeroneconfigure'], - 'htaccess' => ['[{{routerconfigfull}}] create the htaccess file', 'routeronehtaccess'], - 'router' => ['[{{routerconfigfull}}] create the PHP router file', 'routeronerouter'], - 'paths' => ['[{{routerconfigpath}}] modify the paths', 'routeronepaths'], - 'load' => [' load the configuration', 'routeroneload'], - 'save' => ['[{{routerconfigpath}}] save the configuration', 'routeronesave'], - ]); - //$this->cli->addMenuItem('pdooneconnect'); - $this->cli->setVariable('routerconfig', 'pending'); - $this->cli->setVariable('routerconfigpath', 'pending'); - $this->cli->setVariable('routerconfigfull', 'pending'); - $this->cli->addVariableCallBack('router', function(CliOne $cli) { - if ($cli->getValue('routerfilename')) { - $file = true; - $cli->setVariable('routerconfig', 'ok', false); - } else { - $file = false; - $cli->setVariable('routerconfig', 'pending', false); - } - if (count($this->paths) > 0) { - $path = true; - $cli->setVariable('routerconfigpath', 'ok', false); - } else { - $path = false; - $cli->setVariable('routerconfigpath', 'pending', false); - } - if ($file && $path) { - $cli->setVariable('routerconfigfull', 'ok', false); - } else { - $cli->setVariable('routerconfigfull', 'pending', false); - } - }); - $listPHPFiles = $this->getFiles('.', '.config.php'); - $routerFileName = $this->cli->createOrReplaceParam('routerfilename', [], 'longflag') - ->setRequired(false) - ->setCurrentAsDefault() - ->setDescription('select a configuration file to load', 'Select the configuration file to use', [ - 'Example: "--routerfilename myconfig"'] - , 'file') - ->setDefault('') - ->setInput(false, 'string', $listPHPFiles) - ->evalParam(); - $this->routerOneLoad($routerFileName); - if ($run) { - if ($this->cli->getSTDIN() === null) { - $this->showLogo(); - } - $this->cli->evalMenu('mainmenu', $this); - } - } - - public function showLogo(): void - { - echo " _____ _ ____ \n"; - echo " | __ \ | | / __ \ \n"; - echo " | |__) |___ _ _| |_ ___| | | |_ __ ___ \n"; - echo " | _ // _ \| | | | __/ _ \ | | | '_ \ / _ \\\n"; - echo " | | \ \ (_) | |_| | || __/ |__| | | | | __/\n"; - echo " |_| \_\___/ \__,_|\__\___|\____/|_| |_|\___| " . self::VERSION . "\n\n"; - echo "\n"; - } - - public function option(): void - { - $this->cli->createOrReplaceParam('init', [], 'command')->add(); - } - - public function menuRouterOnePaths(): void - { - $this->cli->upLevel('paths'); - //$this->cli->setColor(['byellow'])->showBread(); - while (true) { - $this->cli->setColor(['byellow'])->showBread(); - $this->cli->showValuesColumn($this->paths, 'option'); - $ecc = $this->cli->createOrReplaceParam('extracolumncommand') - ->setAllowEmpty() - ->setInput(true, 'optionshort', ['add', 'remove', 'edit']) - ->setDescription('', 'Select an operation') - ->evalParam(true); - switch ($ecc->value) { - case '': - break 2; - case 'add': - $tmp = $this->cli->createOrReplaceParam('selectpath') - //->setAllowEmpty() - ->setInput() - ->setDescription('', 'Select the name of the path', - ['select an unique name for this path','example:web']) - ->evalParam(true); - $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') - //->setAllowEmpty() - ->setInput() - ->setDescription('', 'Select the path', - ['select the path to be used using the syntax {id:defaultvalue}', - 'example:{controller:Home}/{id}/{idparent} ' - ,'{controller}: the controller' - ,'{action}: the action' - ,'{event}: the event' - ,'{verb}: the verb (GET/POST/etc.)' - ,'{id}: the identifier' - ,'{idparent}: the parent object' - ,'{category}: the category' - ,'{subcategory}: the subcategory' - ,'{subsubcategory}: the subsubcategory']) - ->setDefault('{controller:Home}/{action:list}/{id}/{idparent}') - ->evalParam(true); - $tmp3 = $this->cli->createOrReplaceParam('extracolumn_namespace') - //->setAllowEmpty() - ->setInput() - ->setDescription('', 'Select the namespace associate with the path', - ['example: eftec\\controller']) - ->setDefault('eftec\\controller') - ->evalParam(true); - $this->paths[$tmp->value] = $tmp2->value . ', ' . $tmp3->value; - break; - case 'remove': - $tmp = $this->cli->createOrReplaceParam('extracolumn_delete') - ->setAllowEmpty() - ->setInput(true, 'option', $this->paths) - ->setDescription('', 'Select the column to delete') - ->evalParam(true); - if ($tmp->valueKey !== $this->cli->emptyValue) { - unset($this->paths[$tmp->valueKey]); - } - break; - case 'edit': - $tmp = $this->cli->createOrReplaceParam('extracolumn_edit') - ->setAllowEmpty() - ->setInput(true, 'option', $this->paths) - ->setDescription('', 'Select the column to edit') - ->evalParam(true); - if ($tmp->valueKey !== $this->cli->emptyValue) { - $v = explode(', ', $this->paths[$tmp->valueKey], 2); - $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') - //->setAllowEmpty() - ->setInput() - ->setDescription('', 'Select the path', - ['select the path to be used using the syntax {id:defaultvalue}', - 'example:{controller:Home}/{id}/{idparent} ' - ,'{controller}: the controller' - ,'{action}: the action' - ,'{event}: the event' - ,'{verb}: the verb (GET/POST/etc.)' - ,'{id}: the identifier' - ,'{idparent}: the parent object' - ,'{category}: the category' - ,'{subcategory}: the subcategory' - ,'{subsubcategory}: the subsubcategory']) - ->setDefault($v[0]) - ->evalParam(true); - $tmp3 = $this->cli->createOrReplaceParam('extracolumn_sql2') - //->setAllowEmpty() - ->setInput() - ->setDescription('', 'Select the namespace', ['example: eftec\\controller']) - ->setDefault($v[1]) - ->evalParam(true); - $this->paths[$tmp->valueKey] = $tmp2->value . ', ' . $tmp3->value; - } - break; - } - } - $this->cli->callVariablesCallBack(); - $this->cli->downLevel(2); - } - - /** @noinspection PhpUnused */ - public function menuRouterOneSave(): void - { - $this->cli->upLevel('save'); - $this->cli->setColor(['byellow'])->showBread(); - $sg = $this->cli->createParam('yn', [], 'none') - ->setDescription('', 'Do you want to save the configurations of connection?') - ->setInput(true, 'optionshort', ['yes', 'no']) - ->setDefault('yes') - ->evalParam(true); - if ($sg->value === 'yes') { - $saveconfig = $this->cli->getParameter('routerfilename')->setInput()->evalParam(true); - if ($saveconfig->value) { - $r = $this->cli->saveDataPHPFormat($this->cli->getValue('routerfilename'), $this->getConfig()); - if ($r === '') { - $this->cli->showCheck('OK', 'green', 'file saved correctly'); - } else { - $this->cli->showCheck('ERROR', 'red', 'unable to save file :' . $r); - } - } - } - $this->cli->downLevel(); - } - - /** @noinspection PhpUnused */ - public function menuRouterOneHtaccess(): void - { - $file = $this->cli->getValue('routerfilename') . '.php'; - $content = $this->openTemplate(__DIR__ . '/templates/htaccess_template.php'); - $content = str_replace('route.php', $file, $content); - $this->validateWriteFile('.htaccess', $content); - } - - public function menuRouterOneRouter(): void - { - $config = $this->getConfig(); - $file = $this->cli->getValue('routerfilename') . '.php'; - $content = "openTemplate(__DIR__ . '/templates/route_template.php'); - $namespaces = []; - $paths = []; - foreach ($this->paths as $k => $v) { - $part = explode(', ', $v); - $namespaces[$k] = $part[1]; - $paths[$k] = $part[0]; - } - $content = str_replace([ - '{{baseurldev}}', '{{baseurlprod}}', '{{dev}}', '{{namespaces}}', '{{paths}}' - ], - [ - $config['baseurldev'], $config['baseurlprod'], $config['dev'], var_export($namespaces, true), var_export($paths, true) - ], $content); - $this->validateWriteFile($file, $content); - } - - public function validateWriteFile(string $file, string $content): bool - { - $fail = false; - $exists = @file_exists(getcwd() . '/' . $file); - if ($exists) { - $this->cli->showCheck('warning', 'yellow', "$file file exists, skipping"); - $fail = true; - } else { - $result = @file_put_contents(getcwd() . '/' . $file, $content); - if (!$result) { - $this->cli->showCheck('error', 'red', "Unable to write " . getcwd() . '/' . "$file file\n"); - $fail = true; - } else { - $this->cli->showCheck('ok', 'green', "OK"); - } - } - return $fail; - } - - /** - * @param $filename - * @return false|string - */ - public function openTemplate($filename) - { - $template = @file_get_contents($filename); - if ($template === false) { - throw new RuntimeException("Unable to read template file $filename"); - } - // we delete and replace the first line. - return substr($template, strpos($template, "\n") + 1); - } - - /** @noinspection PhpUnused */ - public function menuRouterOneload(): void - { - $this->cli->upLevel('load'); - $this->cli->setColor(['byellow'])->showBread(); - $routerFileName = $this->cli->getParameter('routerfilename') - ->setInput() - ->evalParam(true); - $this->routerOneLoad($routerFileName); - $this->cli->downLevel(); - } - - public function routerOneLoad(CliOneParam $routerFileName): void - { - if ($routerFileName->value) { - $r = $this->cli->readDataPHPFormat($this->cli->getValue('routerfilename')); - if ($r !== null && $r[0] === true) { - $this->cli->showCheck('OK', 'green', 'file read correctly'); - $this->setConfig($r[1]); - } else { - $this->cli->showCheck('ERROR', 'red', 'unable to read file ' . - $this->cli->getValue('routerfilename') . ", cause " . $r[1]); - } - } - } - - public function menuRouterOneConfigure(): void - { - $this->cli->upLevel('configure'); - $this->cli->setColor(['byellow'])->showBread(); - $this->cli->createOrReplaceParam('routerfilename', [], 'onlyinput') - ->setDescription('The router filename', 'Select the router filename', [ - 'example: router.php']) - ->setInput(true, 'string', 'router.php') - ->setCurrentAsDefault() - ->evalParam(true); - $this->cli->createOrReplaceParam('dev', [], 'none') - ->setDefault(gethostname()) - ->setCurrentAsDefault() - ->setDescription('', "What is the name of your dev machine", [ - 'Select the name of your dev machine', - 'If you don\' know it, then select any information']) - ->setInput() - ->evalParam(true); - $this->cli->createOrReplaceParam('baseurldev', [], 'none') - ->setDefault('http://localhost') - ->setDescription('the base url', 'Select the base url(dev)', - ['Example: https://localhost'], 'baseurldev') - ->setRequired(false) - ->setCurrentAsDefault() - ->setInput() - ->evalParam(true); - $this->cli->createOrReplaceParam('baseurlprod', [], 'none') - ->setDefault('https://www.domain.dom') - ->setDescription('the base url', 'Select the base url(prod)', - ['Example: https://localhost'], 'baseurlprod') - ->setRequired(false) - ->setCurrentAsDefault() - ->setInput() - ->evalParam(true); - $this->cli->callVariablesCallBack(); - $this->cli->downLevel(2); - } - - public function getConfig(): array - { - $r= $this->cli->getValueAsArray(['routerfilename', 'baseurldev', 'baseurlprod', 'dev']); - $r['dev']= $r['dev']==='yes'?gethostname():''; - $r['paths']=$this->paths; - return $r; - } - - public function setConfig(array $array): void - { - $this->paths = $array['paths']; - unset($array['paths']); - $this->cli->setParamUsingArray($array, ['routerfilename', 'baseurldev', 'baseurlprod', 'dev']); - $this->cli->callVariablesCallBack(); - } - - /*** - * It finds the vendor path (where composer is located). - * @param string|null $initPath - * @return string - * - */ - public static function findVendorPath(?string $initPath = null): string - { - $initPath = $initPath ?: __DIR__; - $prefix = ''; - $defaultvendor = $initPath; - // finding vendor - for ($i = 0; $i < 8; $i++) { - if (@file_exists("$initPath/{$prefix}vendor/autoload.php")) { - $defaultvendor = "{$prefix}vendor"; - break; - } - $prefix .= '../'; - } - return $defaultvendor; - } - - /** - * It gets a list of files filtered by extension. - * @param string $path - * @param string $extension . Example: ".php", "php" (it could generate false positives) - * @return array - */ - protected function getFiles(string $path, string $extension): array - { - $scanned_directory = array_diff(scandir($path), ['..', '.']); - $scanned2 = []; - foreach ($scanned_directory as $k) { - $fullname = pathinfo($k)['extension'] ?? ''; - if ($this->str_ends_with($fullname, $extension)) { - $scanned2[$k] = $k; - } - } - return $scanned2; - } - - /** - * for PHP <8.0 compatibility - * @param string $haystack - * @param string $needle - * @return bool - * - */ - protected function str_ends_with(string $haystack, string $needle): bool - { - $needle_len = strlen($needle); - $haystack_len = strlen($haystack); - if ($haystack_len < $needle_len) { - return false; - } - return ($needle_len === 0 || 0 === substr_compare($haystack, $needle, -$needle_len)); - } -} +route = new RouteOne(); + $this->cli = CliOne::instance(); + if (!CliOne::hasMenu()) { + $this->cli->setErrorType(); + $this->cli->addMenu('mainmenu', + function($cli) { + $cli->upLevel('main menu'); + $cli->setColor(['byellow'])->showBread(); + } + , function(CliOne $cli) { + $cli->downLevel(2); + }); + } + $this->cli->addMenuService('mainmenu', $this); + $this->cli->addMenuItem('mainmenu', 'router', + '[{{routerconfigfull}}] Configure the router', 'navigate:routermenu'); + $this->cli->addMenu('routermenu', + function($cli) { + $cli->upLevel('router menu'); + $cli->setColor(['byellow'])->showBread(); + } + , 'footer'); + /** + * The next comments are used to indicate that the methods indicated are used here. + * @see RouteOneCli::menurouteroneconfigure + * @see RouteOneCli::menurouteronehtaccess + * @see RouteOneCli::menurouteronerouter + * @see RouteOneCli::menurouteronepaths + * @see RouteOneCli::menurouteroneload + * @see RouteOneCli::menurouteronesave + */ + $this->cli->addMenuItems('routermenu', [ + 'configure' => ['[{{routerconfig}}] configure and connect to the database', 'routeroneconfigure'], + 'htaccess' => ['[{{routerconfigfull}}] create the htaccess file', 'routeronehtaccess'], + 'router' => ['[{{routerconfigfull}}] create the PHP router file', 'routeronerouter'], + 'paths' => ['[{{routerconfigpath}}] modify the paths', 'routeronepaths'], + 'load' => [' load the configuration', 'routeroneload'], + 'save' => ['[{{routerconfigpath}}] save the configuration', 'routeronesave'], + ]); + //$this->cli->addMenuItem('pdooneconnect'); + $this->cli->setVariable('routerconfig', 'pending'); + $this->cli->setVariable('routerconfigpath', 'pending'); + $this->cli->setVariable('routerconfigfull', 'pending'); + $this->cli->addVariableCallBack('router', function(CliOne $cli) { + if ($cli->getValue('routerfilename')) { + $file = true; + $cli->setVariable('routerconfig', 'ok', false); + } else { + $file = false; + $cli->setVariable('routerconfig', 'pending', false); + } + if (count($this->paths) > 0) { + $path = true; + $cli->setVariable('routerconfigpath', 'ok', false); + } else { + $path = false; + $cli->setVariable('routerconfigpath', 'pending', false); + } + if ($file && $path) { + $cli->setVariable('routerconfigfull', 'ok', false); + } else { + $cli->setVariable('routerconfigfull', 'pending', false); + } + }); + $listPHPFiles = $this->getFiles('.', '.config.php'); + $routerFileName = $this->cli->createOrReplaceParam('routerfilename', [], 'longflag') + ->setRequired(false) + ->setCurrentAsDefault() + ->setDescription('select a configuration file to load', 'Select the configuration file to use', [ + 'Example: "--routerfilename myconfig"'] + , 'file') + ->setDefault('') + ->setInput(false, 'string', $listPHPFiles) + ->evalParam(); + $this->routerOneLoad($routerFileName); + if ($run) { + if ($this->cli->getSTDIN() === null) { + $this->showLogo(); + } + $this->cli->evalMenu('mainmenu', $this); + } + } + + public function showLogo(): void + { + echo " _____ _ ____ \n"; + echo " | __ \ | | / __ \ \n"; + echo " | |__) |___ _ _| |_ ___| | | |_ __ ___ \n"; + echo " | _ // _ \| | | | __/ _ \ | | | '_ \ / _ \\\n"; + echo " | | \ \ (_) | |_| | || __/ |__| | | | | __/\n"; + echo " |_| \_\___/ \__,_|\__\___|\____/|_| |_|\___| " . self::VERSION . "\n\n"; + echo "\n"; + } + + public function option(): void + { + $this->cli->createOrReplaceParam('init', [], 'command')->add(); + } + + public function menuRouterOnePaths(): void + { + $this->cli->upLevel('paths'); + //$this->cli->setColor(['byellow'])->showBread(); + while (true) { + $this->cli->setColor(['byellow'])->showBread(); + $this->cli->showValuesColumn($this->paths, 'option'); + $ecc = $this->cli->createOrReplaceParam('extracolumncommand') + ->setAllowEmpty() + ->setInput(true, 'optionshort', ['add', 'remove', 'edit']) + ->setDescription('', 'Select an operation') + ->evalParam(true); + switch ($ecc->value) { + case '': + break 2; + case 'add': + $tmp = $this->cli->createOrReplaceParam('selectpath') + //->setAllowEmpty() + ->setInput() + ->setDescription('', 'Select the name of the path', + ['select an unique name for this path','example:web']) + ->evalParam(true); + $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') + //->setAllowEmpty() + ->setInput() + ->setDescription('', 'Select the path', + ['select the path to be used using the syntax {id:defaultvalue}', + 'example:{controller:Home}/{id}/{idparent} ' + ,'{controller}: the controller' + ,'{action}: the action' + ,'{event}: the event' + ,'{verb}: the verb (GET/POST/etc.)' + ,'{id}: the identifier' + ,'{idparent}: the parent object' + ,'{category}: the category' + ,'{subcategory}: the subcategory' + ,'{subsubcategory}: the subsubcategory']) + ->setDefault('{controller:Home}/{action:list}/{id}/{idparent}') + ->evalParam(true); + $tmp3 = $this->cli->createOrReplaceParam('extracolumn_namespace') + //->setAllowEmpty() + ->setInput() + ->setDescription('', 'Select the namespace associate with the path', + ['example: eftec\\controller']) + ->setDefault('eftec\\controller') + ->evalParam(true); + $this->paths[$tmp->value] = $tmp2->value . ', ' . $tmp3->value; + break; + case 'remove': + $tmp = $this->cli->createOrReplaceParam('extracolumn_delete') + ->setAllowEmpty() + ->setInput(true, 'option', $this->paths) + ->setDescription('', 'Select the column to delete') + ->evalParam(true); + if ($tmp->valueKey !== $this->cli->emptyValue) { + unset($this->paths[$tmp->valueKey]); + } + break; + case 'edit': + $tmp = $this->cli->createOrReplaceParam('extracolumn_edit') + ->setAllowEmpty() + ->setInput(true, 'option', $this->paths) + ->setDescription('', 'Select the column to edit') + ->evalParam(true); + if ($tmp->valueKey !== $this->cli->emptyValue) { + $v = explode(', ', $this->paths[$tmp->valueKey], 2); + $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') + //->setAllowEmpty() + ->setInput() + ->setDescription('', 'Select the path', + ['select the path to be used using the syntax {id:defaultvalue}', + 'example:{controller:Home}/{id}/{idparent} ' + ,'{controller}: the controller' + ,'{action}: the action' + ,'{event}: the event' + ,'{verb}: the verb (GET/POST/etc.)' + ,'{id}: the identifier' + ,'{idparent}: the parent object' + ,'{category}: the category' + ,'{subcategory}: the subcategory' + ,'{subsubcategory}: the subsubcategory']) + ->setDefault($v[0]) + ->evalParam(true); + $tmp3 = $this->cli->createOrReplaceParam('extracolumn_sql2') + //->setAllowEmpty() + ->setInput() + ->setDescription('', 'Select the namespace', ['example: eftec\\controller']) + ->setDefault($v[1]) + ->evalParam(true); + $this->paths[$tmp->valueKey] = $tmp2->value . ', ' . $tmp3->value; + } + break; + } + } + $this->cli->callVariablesCallBack(); + $this->cli->downLevel(2); + } + + /** @noinspection PhpUnused */ + public function menuRouterOneSave(): void + { + $this->cli->upLevel('save'); + $this->cli->setColor(['byellow'])->showBread(); + $sg = $this->cli->createParam('yn', [], 'none') + ->setDescription('', 'Do you want to save the configurations of connection?') + ->setInput(true, 'optionshort', ['yes', 'no']) + ->setDefault('yes') + ->evalParam(true); + if ($sg->value === 'yes') { + $saveconfig = $this->cli->getParameter('routerfilename')->setInput()->evalParam(true); + if ($saveconfig->value) { + $r = $this->cli->saveDataPHPFormat($this->cli->getValue('routerfilename'), $this->getConfig()); + if ($r === '') { + $this->cli->showCheck('OK', 'green', 'file saved correctly'); + } else { + $this->cli->showCheck('ERROR', 'red', 'unable to save file :' . $r); + } + } + } + $this->cli->downLevel(); + } + + /** @noinspection PhpUnused */ + public function menuRouterOneHtaccess(): void + { + $file = $this->cli->getValue('routerfilename') . '.php'; + $content = $this->openTemplate(__DIR__ . '/templates/htaccess_template.php'); + $content = str_replace('route.php', $file, $content); + $this->validateWriteFile('.htaccess', $content); + } + + public function menuRouterOneRouter(): void + { + $config = $this->getConfig(); + $file = $this->cli->getValue('routerfilename') . '.php'; + $content = "openTemplate(__DIR__ . '/templates/route_template.php'); + $namespaces = []; + $paths = []; + foreach ($this->paths as $k => $v) { + $part = explode(', ', $v); + $namespaces[$k] = $part[1]; + $paths[$k] = $part[0]; + } + $content = str_replace([ + '{{baseurldev}}', '{{baseurlprod}}', '{{dev}}', '{{namespaces}}', '{{paths}}' + ], + [ + $config['baseurldev'], $config['baseurlprod'], $config['dev'], var_export($namespaces, true), var_export($paths, true) + ], $content); + $this->validateWriteFile($file, $content); + } + + public function validateWriteFile(string $file, string $content): bool + { + $fail = false; + $exists = @file_exists(getcwd() . '/' . $file); + if ($exists) { + $this->cli->showCheck('warning', 'yellow', "$file file exists, skipping"); + $fail = true; + } else { + $result = @file_put_contents(getcwd() . '/' . $file, $content); + if (!$result) { + $this->cli->showCheck('error', 'red', "Unable to write " . getcwd() . '/' . "$file file\n"); + $fail = true; + } else { + $this->cli->showCheck('ok', 'green', "OK"); + } + } + return $fail; + } + + /** + * @param $filename + * @return false|string + */ + public function openTemplate($filename) + { + $template = @file_get_contents($filename); + if ($template === false) { + throw new RuntimeException("Unable to read template file $filename"); + } + // we delete and replace the first line. + return substr($template, strpos($template, "\n") + 1); + } + + /** @noinspection PhpUnused */ + public function menuRouterOneload(): void + { + $this->cli->upLevel('load'); + $this->cli->setColor(['byellow'])->showBread(); + $routerFileName = $this->cli->getParameter('routerfilename') + ->setInput() + ->evalParam(true); + $this->routerOneLoad($routerFileName); + $this->cli->downLevel(); + } + + public function routerOneLoad(CliOneParam $routerFileName): void + { + if ($routerFileName->value) { + $r = $this->cli->readDataPHPFormat($this->cli->getValue('routerfilename')); + if ($r !== null && $r[0] === true) { + $this->cli->showCheck('OK', 'green', 'file read correctly'); + $this->setConfig($r[1]); + } else { + $this->cli->showCheck('ERROR', 'red', 'unable to read file ' . + $this->cli->getValue('routerfilename') . ", cause " . $r[1]); + } + } + } + + public function menuRouterOneConfigure(): void + { + $this->cli->upLevel('configure'); + $this->cli->setColor(['byellow'])->showBread(); + $this->cli->createOrReplaceParam('routerfilename', [], 'onlyinput') + ->setDescription('The router filename', 'Select the router filename', [ + 'example: router.php']) + ->setInput(true, 'string', 'router.php') + ->setCurrentAsDefault() + ->evalParam(true); + $this->cli->createOrReplaceParam('dev', [], 'none') + ->setDefault(gethostname()) + ->setCurrentAsDefault() + ->setDescription('', "What is the name of your dev machine", [ + 'Select the name of your dev machine', + 'If you don\' know it, then select any information']) + ->setInput() + ->evalParam(true); + $this->cli->createOrReplaceParam('baseurldev', [], 'none') + ->setDefault('http://localhost') + ->setDescription('the base url', 'Select the base url(dev)', + ['Example: https://localhost'], 'baseurldev') + ->setRequired(false) + ->setCurrentAsDefault() + ->setInput() + ->evalParam(true); + $this->cli->createOrReplaceParam('baseurlprod', [], 'none') + ->setDefault('https://www.domain.dom') + ->setDescription('the base url', 'Select the base url(prod)', + ['Example: https://localhost'], 'baseurlprod') + ->setRequired(false) + ->setCurrentAsDefault() + ->setInput() + ->evalParam(true); + $this->cli->callVariablesCallBack(); + $this->cli->downLevel(2); + } + + public function getConfig(): array + { + $r= $this->cli->getValueAsArray(['routerfilename', 'baseurldev', 'baseurlprod', 'dev']); + $r['dev']= $r['dev']==='yes'?gethostname():''; + $r['paths']=$this->paths; + return $r; + } + + public function setConfig(array $array): void + { + $this->paths = $array['paths']; + unset($array['paths']); + $this->cli->setParamUsingArray($array, ['routerfilename', 'baseurldev', 'baseurlprod', 'dev']); + $this->cli->callVariablesCallBack(); + } + + /*** + * It finds the vendor path (where composer is located). + * @param string|null $initPath + * @return string + * + */ + public static function findVendorPath(?string $initPath = null): string + { + $initPath = $initPath ?: __DIR__; + $prefix = ''; + $defaultvendor = $initPath; + // finding vendor + for ($i = 0; $i < 8; $i++) { + if (@file_exists("$initPath/{$prefix}vendor/autoload.php")) { + $defaultvendor = "{$prefix}vendor"; + break; + } + $prefix .= '../'; + } + return $defaultvendor; + } + + /** + * It gets a list of files filtered by extension. + * @param string $path + * @param string $extension . Example: ".php", "php" (it could generate false positives) + * @return array + */ + protected function getFiles(string $path, string $extension): array + { + $scanned_directory = array_diff(scandir($path), ['..', '.']); + $scanned2 = []; + foreach ($scanned_directory as $k) { + $fullname = pathinfo($k)['extension'] ?? ''; + if ($this->str_ends_with($fullname, $extension)) { + $scanned2[$k] = $k; + } + } + return $scanned2; + } + + /** + * for PHP <8.0 compatibility + * @param string $haystack + * @param string $needle + * @return bool + * + */ + protected function str_ends_with(string $haystack, string $needle): bool + { + $needle_len = strlen($needle); + $haystack_len = strlen($haystack); + if ($haystack_len < $needle_len) { + return false; + } + return ($needle_len === 0 || 0 === substr_compare($haystack, $needle, -$needle_len)); + } +} diff --git a/lib/routeonecli b/lib/routeonecli index fee4beb..931e851 100644 --- a/lib/routeonecli +++ b/lib/routeonecli @@ -1,24 +1,24 @@ - - - - Options -MultiViews -Indexes - - - RewriteEngine On - DirectoryIndex route.php - - # Handle Authorization Header - RewriteCond %{HTTP:Authorization} . - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - - # Redirect Trailing Slashes If Not A Folder... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_URI} (.+)/$ - RewriteRule ^ %1 [L,R=301] - - # Send Requests To Front Controller... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^(.*)$ route.php?req=$1 [L,QSA] - + + + + Options -MultiViews -Indexes + + # based in Laravel .htaccess + RewriteEngine On + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To router + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + #RewriteRule ^ index.php [L] + RewriteRule ^(.*)$ index.php?req=$1 [L] + diff --git a/lib/templates/route_template.php b/lib/templates/route_template.php index 50d1b3e..9be2197 100644 --- a/lib/templates/route_template.php +++ b/lib/templates/route_template.php @@ -1,48 +1,48 @@ - -use eftec\routeone\RouteOne; - -include __DIR__ . "/vendor/autoload.php"; -/** - * Generate by RouteOne.php - */ -function configureRouteOne():RouteOne -{ - if (gethostname() === '{{dev}}') { - $baseurl= "{{baseurldev}}"; // dev url - } else { - $baseurl="{{baseurlprod}}"; // prod url - } - $routeNS={{namespaces}}; - $routePath={{paths}}; - $route = new RouteOne($baseurl); - foreach ($routePath as $k => $v) { - $route->addPath($v, $k); - } - $route->fetchPath(); - /* todo: we could do some auth work here. - if($auth===null && $route->controller=="login") { - $route->redirect("xxxx"); - } - */ - try { - // it will be the class somenamespace\ControllerNameController::actionAction - $found = false; - foreach ($routeNS as $k => $namespace) { - if ($route->currentPath === $k) { - $found = true; - $route->callObjectEx($namespace . "\{controller}Controller"); - } - } - if (!$found) { - http_response_code(404); - die(1); - } - } catch (Exception $e) { - echo $e->getMessage(); - http_response_code($e->getCode()); - die(1); - } - return $route; -} -configureRouteOne(); - + +use eftec\routeone\RouteOne; + +include __DIR__ . "/vendor/autoload.php"; +/** + * Generate by RouteOne.php + */ +function configureRouteOne():RouteOne +{ + if (gethostname() === '{{dev}}') { + $baseurl= "{{baseurldev}}"; // dev url + } else { + $baseurl="{{baseurlprod}}"; // prod url + } + $routeNS={{namespaces}}; + $routePath={{paths}}; + $route = new RouteOne($baseurl); + foreach ($routePath as $k => $v) { + $route->addPath($v, $k); + } + $route->fetchPath(); + /* todo: we could do some auth work here. + if($auth===null && $route->controller=="login") { + $route->redirect("xxxx"); + } + */ + try { + // it will be the class somenamespace\ControllerNameController::actionAction + $found = false; + foreach ($routeNS as $k => $namespace) { + if ($route->currentPath === $k) { + $found = true; + $route->callObjectEx($namespace . "\{controller}Controller"); + } + } + if (!$found) { + http_response_code(404); + die(1); + } + } catch (Exception $e) { + echo $e->getMessage(); + http_response_code($e->getCode()); + die(1); + } + return $route; +} +configureRouteOne(); +