<?php
namespace Lia\Compo;
class Router extends \Lia\Compo {
protected $varDelim = '\\.\\/';
protected $routeMap = [];
/**
*
* Documentation
* - Document the paramaters that are passed to a public file
* - document examples of routing
* - improve 'Pattern Rules' documentation
*
* Essentials
* - figure out sitemaps... Though that may not be part of router??
* - add... something to help convert pattern-based routes to sitemap representations
*
* Performance
* - caching of urls & routes
*
* Other
* - auto-add `public` dir for each package
* - Right now, the package handles this. Should the Router be able to process a directory? (probably)
* - add a function to convert a file path to a pattern (aka, remove extensions like '.md' and '.php', per a configuration)
* - add a normalizeUrl function (add trailing slash, remove .php / .html / .md, all lowercase?)
* - Option to return both perfect-match routes AND routes that match with the normalized url, aka: /some/url && /some/url/ would return the same route(s)
* - add a mediator function to handle multiple global routers, possibly??
*
* @export(TODO.Router)
*/
//
// Things i need to DO
//
// protected function normalizeUrl($url){
// $ext = pathinfo($url,PATHINFO_EXTENSION);
// $hiddenExts = ['php'];
// if ($removeDoubleSlash=true){
// while (strpos($url,'//')!==FALSE)
// $url = str_replace(['////','///','//'],'/',$url);
// }
// if ($toLowerCase=true){
// $url = strtolower($url);
// }
// foreach ($hiddenExts as $hiddenExt){
// $hiddenExt = '.'.$hiddenExt;
// $hideIt = true;
// if ($hideIt
// &&strtolower(substr($url,-(strlen($hiddenExt))))==strtolower($hiddenExt)){
// $pos = strrpos($url,$hiddenExt);
// $remainder = substr($url,$pos+strlen($hiddenExt));
// $url = substr($url,0,$pos).$remainder;
// $ext = '';
// }
// }
// if ($forceTrailingSlash=true
// &&substr($url,-1)!=='/'){
// $url .= '/';
// }
// if ($removeTrailingSlashIfExt=true
// &&$ext!=''){
// while (substr($url,-1)==='/')
// $url = substr($url,0,-1);
// }
// return $url;
// }
//
// APIs
//
public function apiPrefix_lia_route_routePattern_deriveRouteUrls($key, $callable){
$patterns = $callable(false);
// print_r($patterns);
foreach ($patterns as $pattern){
$this->lia->api('lia.route', 'add', $pattern, $callable);
}
}
/**
* Call `$lia->addRoute($pattern, $callbackOrFile,$package=null)` to add a route to the built-in router
*
* ```
* @param $pattern A pattern. See [the pattern matching rules](@link(Rules.Router.pattern))
* @param $callbackOrFile a callback or a file path
* @param $package (optional) A liaison package
* ```
* @export(Usage.Router.addRoute)
* @TODO make addcallback & addfile separate functions, probably
*/
public function apiAdd_lia_route_addRoute($pattern, $callbackOrFile,$package=null){
$initialParsed = $this->parsePattern($pattern);
$list = [$initialParsed];
if (($initialParsed['extraParsedPattern']??false)){
$extraParsed = $initialParsed;
$extraParsed['parsedPattern'] = $extraParsed['extraParsedPattern'];
$params = $extraParsed['params'];
foreach ($extraParsed['optionalParams'] as $p){
$index = array_search($p, $params);
unset($params[$index]);
}
$params = array_values($params);
$extraParsed['params'] = $params;
$list[] = $extraParsed;
}
foreach ($list as $parsed){
$parsed['target'] = $callbackOrFile;
$parsed['package'] = $package;
$testPattern = $parsed['parsedPattern'];
$matches = [];
foreach ($parsed['methods'] as $m){
$this->routeMap[$m][$testPattern][] = $parsed;
}
}
}
/**
*
* @export(Usage.Router.getRoute)
*/
public function apiGet_lia_route_route(\Lia\Obj\Request $request){
$url = $request->url();
$method = $request->method();
$testReg = $this->urlToTestReg($url);
$all = array_filter($this->routeMap[$method]??[],
function($routeList,$parsedPattern) use ($testReg) {
if (preg_match('/'.$testReg.'/',$parsedPattern))return true;
return false;
}
,ARRAY_FILTER_USE_BOTH);
$routeList = [];
sort($all);
$all = array_merge(...$all);
foreach ($all as $routeInfo){
$active = [
'url' => $url,
'method'=>$method,
'urlRegex'=>$testReg
];
$paramaters = null;
$paramaters = $this->mapParamaters($routeInfo, $url);
$optionalParamaters = $routeInfo['optionalParams']??[];
$shared = [
'paramaters'=>$paramaters,
'optionalParamaters'=> $optionalParamaters,
];
$static = [
'allowedMethods'=>$routeInfo['methods'],
'paramaterizedPattern'=>$routeInfo['pattern'],
'placeholderPattern'=>$routeInfo['parsedPattern'],
'target'=>$routeInfo['target'],
'package'=>$routeInfo['package'],
];
$route = new \Lia\Obj\Route(array_merge($active,$shared,$static));
$routeList[] = $route;
}
return $routeList;
}
//
// utility Functions
//
/**
* ```
* rules:
* .php will generally be removed & replaced with a trailing slash, but that is NOT part of parsePattern()
* That will be a pattern-normalization step that happens prior to parsePattern() and is extensible/configurable
*
* Methods: @POST, @GET, @PUT, @DELETE, @OPTIONS, @TRACE, @HEAD, @CONNECT
* - We do not currently check the name of the method, just @ABCDEF for length 3-7
* - These must appear after a `/` or after another '@METHOD.' or they will be taken literally
* - lower case is not valid
* - Each method MUST be followed by a period (.)
* - example: /@POST.dir/sub/@GET.file/ is valid for both POST /dir/sub/file/ and GET /dir/sub/file
*
* Paramaters:
* - {under_scoreCamel} specifies a named, dynamic paramater
* - {param} must be surrounded by path delimiters (/) OR periods (.) which will be literal characters in the url
* - {param} MAY be at the end of a pattern with no trailing delimiter
* - {paramName:regex} would specify a dynamic portion of a url that MUST match the given regex.
* - Not currently implemented
* - {?param} would specify a paramater that is optional
* - Not currently implemented
* examples:
* /blog/{category}/{post} is valid for url /blog/black-lives/matter
* /blog/{category}.{post}/ is valid for url /blog/environment.zero-waste/
* /blog/{category}{post}/ is valid for url /blog/{category}{post}/ and has NO dynamic paramaters
* ```
* @export(Rules.Router.pattern)
*/
public function parsePattern($pattern){
$dlm = $this->varDelim;
$params = [];
$optionalParams = [];
$replace =
function($matches) use (&$params, &$optionalParams){
if ($matches[1]=='?'){
$params[] = $matches[2];
$optionalParams[] = $matches[2];
return '#';
}
$params[] = $matches[2];
return '?';
};
$pieces = explode('/',$pattern);
$methods = [];
$testUrl = '';
$extraTestUrl = '';
foreach ($pieces as $piece){
$startPiece = $piece;
// extract @METHODS.
while (preg_match('/^\@([A-Z]{3,7})\./',$piece,$methodMatches)){
$method = $methodMatches[1];
$len = strlen($method);
$piece = substr($piece,2+$len);
$methods[$methodMatches[1]] = $methodMatches[1];
}
while ($piece!=($piece = preg_replace_callback('/(?<=^|['.$dlm.'])\{(\??)([a-zA-Z\_]+)\}(?=['.$dlm.']|$)/',$replace,$piece))){
}
if ($piece=='#'&&$startPiece!=$piece){
$extraTestUrl .= '';// don't add anything.
$piece = '?';
} else {
$extraTestUrl .= '/'.$piece;
}
$testUrl .= '/'.$piece;
}
$testUrl = str_replace(['///','//'],'/',$testUrl);
$extraTestUrl = str_replace(['///','//'],'/',$extraTestUrl);
$parsed = [
'pattern'=>$pattern,
'parsedPattern'=>$testUrl,
'params'=>$params,
'methods'=>count($methods)>0 ? $methods : ['GET'=>'GET'],
];
if ($testUrl!=$extraTestUrl){
$parsed['extraParsedPattern']=$extraTestUrl;
$parsed['optionalParams'] = $optionalParams;
}
return $parsed;
}
/**
* @param $parsedPattern expects the array generated by parsePattern(/url/pattern/)
*/
public function mapParamaters($parsedPattern, $url){
$phPattern = $parsedPattern['parsedPattern'];
$staticPieces = explode('?',$phPattern);
$staticPieces = array_map(function($piece){return preg_quote($piece,'/');}, $staticPieces);
$dlm = $this->varDelim;
$reg = "([^{$dlm}].*)";
$asReg = '/'.implode($reg, $staticPieces).'/';
preg_match($asReg,$url,$matches);
$params = [];
$i=1;
foreach ($parsedPattern['params'] as $name){
$params[$name] = $matches[$i++] ?? null;
if ($params[$name]==null){
echo "\n\nInternal Error. Please report a bug on https://github.com/Taeluf/Liaison/issues with the following:\n\n";
echo "url: {$url}\nParsed Pattern:\n";
print_r($parsedPattern);
echo "\n\n";
throw new \Lia\Exception\Base("Internal error. We were unable to perform routing for '{$url}'. ");
}
}
return $params;
}
public function urlToTestReg($url){
$dlm = $this->varDelim;
$pieces = preg_split('/['.$dlm.']/',$url);
array_shift($pieces);
$last = array_pop($pieces);
if ($last!='')$pieces[] = $last;
$reg = '';
$pos = 0;
$test = '';
foreach ($pieces as $p){
$len = strlen($p)+1;
$startDelim = substr($url,$pos,1);
if ($p==''){
$test .= '\\'.$startDelim;
$pos += $len;
continue;
}
$pEsc = preg_quote($p,'/');
$pReg = '\\'.$startDelim.'(?:'.$pEsc.'|\?)';
$pos += $len;
$test .= $pReg;
}
$finalDelim = substr($url,$pos);
$test .= $finalDelim ? '\\'.$finalDelim : '';
$test = '^'.$test.'$';
return $test;
}
}