mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +00:00
558 lines
16 KiB
Plaintext
558 lines
16 KiB
Plaintext
|
|
namespace Test\Router;
|
|
|
|
/**
|
|
* Test\Router\Route
|
|
*
|
|
* This class represents every route added to the router
|
|
*/
|
|
class Route
|
|
{
|
|
protected _pattern;
|
|
|
|
protected _compiledPattern;
|
|
|
|
protected _paths;
|
|
|
|
protected _methods;
|
|
|
|
protected _hostname;
|
|
|
|
protected _converters;
|
|
|
|
protected _id;
|
|
|
|
protected _name;
|
|
|
|
protected _beforeMatch;
|
|
|
|
/**
|
|
* Test\Router\Route constructor
|
|
*
|
|
* @param string pattern
|
|
* @param array paths
|
|
* @param array|string httpMethods
|
|
*/
|
|
public function __construct(pattern, paths=null, httpMethods=null)
|
|
{
|
|
// Configure the route (extract parameters, paths, etc)
|
|
this->reConfigure(pattern, paths);
|
|
|
|
// Update the HTTP method constraints
|
|
let this->_methods = httpMethods;
|
|
}
|
|
|
|
/**
|
|
* Replaces placeholders from pattern returning a valid PCRE regular expression
|
|
*
|
|
* @param string pattern
|
|
* @return string
|
|
*/
|
|
public function compilePattern(pattern)
|
|
{
|
|
var idPattern;
|
|
|
|
// If a pattern contains ':', maybe there are placeholders to replace
|
|
if memstr(pattern, ":") {
|
|
|
|
// This is a pattern for valid identifiers
|
|
let idPattern = "/([a-zA-Z0-9\\_\\-]+)";
|
|
|
|
// Replace the module part
|
|
if memstr(pattern, "/:module") {
|
|
let pattern = str_replace("/:module", idPattern, pattern);
|
|
}
|
|
|
|
// Replace the controller placeholder
|
|
if memstr(pattern, "/:controller") {
|
|
let pattern = str_replace("/:controller", idPattern, pattern);
|
|
}
|
|
|
|
// Replace the namespace placeholder
|
|
if memstr(pattern, "/:namespace") {
|
|
let pattern = str_replace("/:namespace", idPattern, pattern);
|
|
}
|
|
|
|
// Replace the action placeholder
|
|
if memstr(pattern, "/:action") {
|
|
let pattern = str_replace("/:action", idPattern, pattern);
|
|
}
|
|
|
|
// Replace the params placeholder
|
|
if memstr(pattern, "/:params") {
|
|
let pattern = str_replace("/:params", "(/.*)*", pattern);
|
|
}
|
|
|
|
// Replace the int placeholder
|
|
if memstr(pattern, "/:int") {
|
|
let pattern = str_replace("/:int", "/([0-9]+)", pattern);
|
|
}
|
|
}
|
|
|
|
// Check if the pattern has parantheses in order to add the regex delimiters
|
|
if memstr(pattern, "(") {
|
|
return "#^" . pattern . "$#";
|
|
}
|
|
|
|
// Square brackets are also checked
|
|
if memstr(pattern, "[") {
|
|
return "#^" . pattern . "$#";
|
|
}
|
|
|
|
return pattern;
|
|
}
|
|
|
|
/**
|
|
* Set one or more HTTP methods that constraint the matching of the route
|
|
*
|
|
*<code>
|
|
* $route->via('GET');
|
|
* $route->via(array('GET', 'POST'));
|
|
*</code>
|
|
*
|
|
* @param string|array httpMethods
|
|
* @return Test\Router\Route
|
|
*/
|
|
public function via(httpMethods)
|
|
{
|
|
let this->_methods = httpMethods;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Extracts parameters from a string
|
|
*
|
|
* @param string pattern
|
|
*/
|
|
public function extractNamedParams(string pattern)
|
|
{
|
|
|
|
char ch;
|
|
var tmp, matches;
|
|
boolean notValid = false;
|
|
int cursor, cursorVar, marker, bracketCount = 0, parenthesesCount = 0, foundPattern = 0;
|
|
int intermediate = 0, numberMatches = 0;
|
|
string route, item, variable, regexp;
|
|
|
|
if strlen(pattern) <= 0 {
|
|
return false;
|
|
}
|
|
|
|
let matches = [],
|
|
route = "";
|
|
|
|
for cursor, ch in pattern {
|
|
|
|
if parenthesesCount == 0 {
|
|
if ch == '{' {
|
|
if bracketCount == 0 {
|
|
let marker = cursor + 1,
|
|
intermediate = 0,
|
|
notValid = false;
|
|
}
|
|
let bracketCount++;
|
|
} else {
|
|
if ch == '}' {
|
|
let bracketCount--;
|
|
if intermediate > 0 {
|
|
if bracketCount == 0 {
|
|
|
|
let numberMatches++,
|
|
variable = null,
|
|
regexp = null,
|
|
item = (string) substr(pattern, marker, cursor - marker);
|
|
|
|
for cursorVar, ch in item {
|
|
|
|
if ch == '\0' {
|
|
break;
|
|
}
|
|
|
|
if cursorVar == 0 && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
|
|
let notValid = true;
|
|
break;
|
|
}
|
|
|
|
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <='9') || ch == '-' || ch == '_' || ch == ':' {
|
|
if ch == ':' {
|
|
let variable = (string) substr(item, 0, cursorVar),
|
|
regexp = (string) substr(item, cursorVar + 1);
|
|
break;
|
|
}
|
|
} else {
|
|
let notValid = true;
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if !notValid {
|
|
|
|
let tmp = numberMatches;
|
|
|
|
if variable && regexp {
|
|
|
|
let foundPattern = 0;
|
|
for ch in regexp {
|
|
if ch == '\0' {
|
|
break;
|
|
}
|
|
if !foundPattern {
|
|
if ch == '(' {
|
|
let foundPattern = 1;
|
|
}
|
|
} else {
|
|
if ch == ')' {
|
|
let foundPattern = 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundPattern != 2 {
|
|
let route .= '(',
|
|
route .= regexp,
|
|
route .= ')';
|
|
} else {
|
|
let route .= regexp;
|
|
}
|
|
let matches[variable] = tmp;
|
|
} else {
|
|
let route .= "([^/]*)",
|
|
matches[item] = tmp;
|
|
}
|
|
} else {
|
|
let route .= "{" . item ."}";
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bracketCount == 0 {
|
|
if ch == '(' {
|
|
let parenthesesCount++;
|
|
} else {
|
|
if ch == ')' {
|
|
let parenthesesCount--;
|
|
if parenthesesCount == 0 {
|
|
let numberMatches++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bracketCount > 0 {
|
|
let intermediate++;
|
|
} else {
|
|
let route .= ch;
|
|
}
|
|
}
|
|
|
|
return [route, matches];
|
|
}
|
|
|
|
/**
|
|
* Reconfigure the route adding a new pattern and a set of paths
|
|
*
|
|
* @param string pattern
|
|
* @param array paths
|
|
*/
|
|
public function reConfigure(pattern, paths=null)
|
|
{
|
|
var moduleName, controllerName, actionName,
|
|
parts, routePaths, realClassName, namespaceName,
|
|
pcrePattern, compiledPattern, extracted;
|
|
|
|
if typeof pattern != "string" {
|
|
throw new Exception("The pattern must be string");
|
|
}
|
|
|
|
if paths !== null {
|
|
if typeof paths == "string" {
|
|
|
|
let moduleName = null,
|
|
controllerName = null,
|
|
actionName = null;
|
|
|
|
// Explode the short paths using the :: separator
|
|
let parts = explode("::", paths);
|
|
|
|
// Create the array paths dynamically
|
|
switch count(parts) {
|
|
case 3:
|
|
let moduleName = parts[0],
|
|
controllerName = parts[1],
|
|
actionName = parts[2];
|
|
break;
|
|
case 2:
|
|
let controllerName = parts[0],
|
|
actionName = parts[1];
|
|
break;
|
|
case 1:
|
|
let controllerName = parts[0];
|
|
break;
|
|
}
|
|
|
|
let routePaths = [];
|
|
|
|
// Process module name
|
|
if moduleName !== null {
|
|
let routePaths["module"] = moduleName;
|
|
}
|
|
|
|
// Process controller name
|
|
if controllerName !== null {
|
|
|
|
// Check if we need to obtain the namespace
|
|
if memstr(controllerName, "\\") {
|
|
|
|
// Extract the real class name from the namespaced class
|
|
let realClassName = get_class_ns(controllerName);
|
|
|
|
// Extract the namespace from the namespaced class
|
|
let namespaceName = get_ns_class(controllerName);
|
|
|
|
// Update the namespace
|
|
if namespaceName {
|
|
let routePaths["namespace"] = namespaceName;
|
|
}
|
|
} else {
|
|
let realClassName = controllerName;
|
|
}
|
|
|
|
// Always pass the controller to lowercase
|
|
let routePaths["controller"] = uncamelize(realClassName);
|
|
}
|
|
|
|
// Process action name
|
|
if actionName !== null {
|
|
let routePaths["action"] = actionName;
|
|
}
|
|
} else {
|
|
let routePaths = paths;
|
|
}
|
|
} else {
|
|
let routePaths = [];
|
|
}
|
|
|
|
if typeof routePaths !== "array" {
|
|
throw new Exception("The route contains invalid paths");
|
|
}
|
|
|
|
// If the route starts with '#' we assume that it is a regular expression
|
|
if !starts_with(pattern, "#") {
|
|
|
|
if memstr(pattern, "{") {
|
|
// The route has named parameters so we need to extract them
|
|
let extracted = this->extractNamedParams(pattern),
|
|
pcrePattern = extracted[0],
|
|
routePaths = array_merge(routePaths, extracted[1]);
|
|
} else {
|
|
let pcrePattern = pattern;
|
|
}
|
|
|
|
// Transform the route's pattern to a regular expression
|
|
let compiledPattern = this->compilePattern(pcrePattern);
|
|
} else {
|
|
let compiledPattern = pattern;
|
|
}
|
|
|
|
// Update the original pattern
|
|
let this->_pattern = pattern;
|
|
|
|
// Update the compiled pattern
|
|
let this->_compiledPattern = compiledPattern;
|
|
|
|
//Update the route's paths
|
|
let this->_paths = routePaths;
|
|
}
|
|
|
|
/**
|
|
* Returns the route's name
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getName()
|
|
{
|
|
return this->_name;
|
|
}
|
|
|
|
/**
|
|
* Sets the route's name
|
|
*
|
|
*<code>
|
|
* $router->add('/about', array(
|
|
* 'controller' => 'about'
|
|
* ))->setName('about');
|
|
*</code>
|
|
*
|
|
* @param string name
|
|
* @return Route
|
|
*/
|
|
public function setName(name)
|
|
{
|
|
let this->_name = name;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets a callback that is called if the route is matched.
|
|
* The developer can implement any arbitrary conditions here
|
|
* If the callback returns false the route is treaded as not matched
|
|
*
|
|
* @param callback callback
|
|
* @return Test\Router\Route
|
|
*/
|
|
public function beforeMatch(callback)
|
|
{
|
|
let this->_beforeMatch = callback;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the 'before match' callback if any
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getBeforeMatch()
|
|
{
|
|
return this->_beforeMatch;
|
|
}
|
|
|
|
/**
|
|
* Returns the route's id
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRouteId()
|
|
{
|
|
return this->_id;
|
|
}
|
|
|
|
/**
|
|
* Returns the route's pattern
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getPattern()
|
|
{
|
|
return this->_pattern;
|
|
}
|
|
|
|
/**
|
|
* Returns the route's compiled pattern
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCompiledPattern()
|
|
{
|
|
return this->_compiledPattern;
|
|
}
|
|
|
|
/**
|
|
* Returns the paths
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getPaths()
|
|
{
|
|
return this->_paths;
|
|
}
|
|
|
|
/**
|
|
* Returns the paths using positions as keys and names as values
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getReversedPaths()
|
|
{
|
|
var reversed, path, position;
|
|
|
|
let reversed = [];
|
|
for path, position in this->_paths {
|
|
let reversed[position] = path;
|
|
}
|
|
return reversed;
|
|
}
|
|
|
|
/**
|
|
* Sets a set of HTTP methods that constraint the matching of the route (alias of via)
|
|
*
|
|
*<code>
|
|
* $route->setHttpMethods('GET');
|
|
* $route->setHttpMethods(array('GET', 'POST'));
|
|
*</code>
|
|
*
|
|
* @param string|array httpMethods
|
|
* @return Test\Router\Route
|
|
*/
|
|
public function setHttpMethods(httpMethods)
|
|
{
|
|
let this->_methods = httpMethods;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the HTTP methods that constraint matching the route
|
|
*
|
|
* @return string|array
|
|
*/
|
|
public function getHttpMethods()
|
|
{
|
|
return this->_methods;
|
|
}
|
|
|
|
/**
|
|
* Sets a hostname restriction to the route
|
|
*
|
|
*<code>
|
|
* $route->setHostname('localhost');
|
|
*</code>
|
|
*
|
|
* @param string|array httpMethods
|
|
* @return Test\Router\Route
|
|
*/
|
|
public function setHostname(hostname)
|
|
{
|
|
let this->_hostname = hostname;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the hostname restriction if any
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getHostname()
|
|
{
|
|
return this->_hostname;
|
|
}
|
|
|
|
/**
|
|
* Adds a converter to perform an additional transformation for certain parameter
|
|
*
|
|
* @param string name
|
|
* @param callable converter
|
|
* @return Test\Router\Route
|
|
*/
|
|
public function convert(name, converter)
|
|
{
|
|
let this->_converters[name] = converter;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the router converter
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getConverters()
|
|
{
|
|
return this->_converters;
|
|
}
|
|
|
|
}
|