Files
linguist/samples/Zephir/Router.zep
2014-04-15 20:13:32 +04:00

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;
}
}