From 8d42991e2dad0d3deb8f7b50276a1eb452be3ced Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Tue, 29 Nov 2016 02:14:54 -0500 Subject: [PATCH] Add the Router and RouteMap components for http2. These are similar to the UrlMap and FrontController request processor in http, but have been adapted to the Pipeline/Middleware architecture. --- http2/action.php | 50 ++++++++++++++ http2/pipeline.php | 60 +++++++---------- http2/route_map.php | 155 ++++++++++++++++++++++++++++++++++++++++++++ http2/router.php | 71 ++++++++++++++++++++ 4 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 http2/action.php create mode 100644 http2/route_map.php create mode 100644 http2/router.php diff --git a/http2/action.php b/http2/action.php new file mode 100644 index 0000000..68a8788 --- /dev/null +++ b/http2/action.php @@ -0,0 +1,50 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class Action extends Middleware { + protected $request; + + public function beforeRequest() {} + + public function afterRequest(Response $response) {} + + public function defaultRequest(Request $request) { + return $this->next->execute($request); + } + + public function execute(Request $request) { + $this->request = $request; + + $this->beforeRequest(); + + $action = @$request->context[Router::class]['data']['action']; + if (!$action || !method_exists($this, $action)) { + return $this->defaultRequest($request); + } + + $response = $this->{$action}(); + + $this->afterRequest($response); + + return $response; + } +} diff --git a/http2/pipeline.php b/http2/pipeline.php index 5d7f885..96c1e2f 100644 --- a/http2/pipeline.php +++ b/http2/pipeline.php @@ -24,33 +24,38 @@ class Pipeline { private $middleware = []; public function add($middleware) { - /* - if (!($middleware instanceof Middleware || - $middleware instanceof \Closure || - is_array($middleware))) { - // TODO(php7): TypeError - throw new \Exception(__FUNCTION__ . ' requires a Middleware or Closure'); - } - */ - $this->middleware[] = $middleware; + array_push($this->middleware, $middleware); return $this; } public function build() { + return $this->_build(new Sentinel(new NotFoundResponse())); + } + + public function buildWithTail($tail) { $chain = new Sentinel(new NotFoundResponse()); - for ($i = count($this->middleware) - 1; $i >= 0; --$i) { - $args = $this->middleware[$i]; - if ($args instanceof \Closure) { - $chain = new ClosureMiddleware($this, $chain, $args); - } else if (is_array($args)) { - $class = array_shift($args); - array_unshift($args, $this, $chain); - $chain = new $class(...$args); - } else { - $chain = new $args($this, $chain); - } + $chain = $this->buildMiddleware($tail, $chain); + return $this->_build($chain); + } + + public function buildMiddleware($args, $next) { + if ($args instanceof \Closure) { + return new ClosureMiddleware($this, $next, $args); + } else if ($args instanceof Pipeline) { + return $args->_build($next); + } else if (is_array($args)) { + $class = array_shift($args); + array_unshift($args, $this, $next); + return new $class(...$args); + } else { + return new $args($this, $next); } + } + private function _build($chain) { + for ($i = count($this->middleware) - 1; $i >= 0; --$i) { + $chain = $this->buildMiddleware($this->middleware[$i], $chain); + } return $chain; } @@ -63,18 +68,3 @@ class Pipeline { $this->run($this->build()); } } - -class SubPipeline extends Middleware { - public function __construct(Pipeline $subpipe) { - // Skip calling super since this is a new pipeline and the - // next middleware will be selected by building the subpipe. - // It would be possible to build the middleware chain here, but - // doing it lazily on execution is more efficient, since SubPipeline - // should be used to partition applications. - $this->subpipe = $subpipe; - } - - public function execute(Request $request) { - return $this->subpipe->build()->execute($request); - } -} diff --git a/http2/route_map.php b/http2/route_map.php new file mode 100644 index 0000000..92f00a1 --- /dev/null +++ b/http2/route_map.php @@ -0,0 +1,155 @@ +. + +namespace hoplite\http2; + +/*! + A RouteMap parses a HTTP Request URL, matches it against its set of rules, and + finds one that matches. If it is successful, data are extracted from the URL, + based on the specified pattern. + + The map is an associative array of URL prefix patterns to a result value. + When used with the Router Middleware, the value should be a Middleware that + can be built by Pipeline::buildMiddleware(). An Action subclass typical. + + The keys can be either a URL prefix or a regular expression. + + For URL prefixes, the pattern is matched relative to the Request::url field. + Patterns should not begin with a slash. Path fragment parameter extraction + can be performed as well. For example, the pattern 'user/view/{id}' will + match a URL like + http://example.com/webapp/user/view/42 + And the match result data will have a key called 'id' with value 42. + + Typically prefix matching will be done for the above patterns. This may be + unwanted if a more general path needs to execute before a more descendent + one. Using strict matching, invoked by ending a pattern with two slashes, + pattern matching becomes exact in that all path components must match. + + Regular expression matching is not limited to prefix patterns and can match + any part of the URL (though prefix matching can be enforced using the + the standard regex '^' character). Regex patterns must begin and end with + '/' characters. During evaluation, if any pattern groups are matched, the + resulting matches will be placed in Request data via the 'url_pattern' key. + The following will match the same URL as above: + '/^user\/view\/([0-9]+)/' +*/ +class RouteMap { + /*! @var array The map of URLs to actions. */ + private $map; + + public function __construct(array $map) { + $this->map = $map; + } + + /*! @brief Evalutes the URL map and finds a match. + This will take the incoming URL from the request and will match it against + the patterns in the internal map. + + Matching occurs in a linear scan of the URL map, so precedence can be + enforced by ordering rules appropriately. Regular expressions are matched + and URL parameter extraction occurs each time a different rule is evaluated. + + If a match is made, an associative array with this stucture is returned: + [ + 'result' => , + 'data' => [ ], + 'regexp' => + ] + + @return array|NULL A matched value in the ::map or NULL if no match. + */ + public function match(Request $request) { + $fragments = explode('/', $request->url); + $path_length = strlen($request->url); + + $result = []; + + foreach ($this->map as $rule => $action) { + // First check if this is the empty rule. + if (empty($rule)) { + if (empty($request->url)) + return [ 'result' => $action ]; + else + continue; + } + + // Check if this is a regular expression rule and match it. + if ($rule[0] == '/' && substr($rule, -1) == '/') { + $matches = array(); + if (preg_match($rule, $request->url, $matches)) { + // This pattern matched, so fill out the request and return. + return [ + 'result' => $action, + 'regexp' => $matches, + ]; + } + } + // Otherwise, this is just a normal string match. + else { + // Patterns that end with two slashes are exact. + $is_strict = substr($rule, -2) == '//'; + if ($is_strict) + $rule = substr($rule, 0, -2); + + // Set up some variables for the loop. + $is_match = TRUE; + $rule_fragments = explode('/', $rule); + $count_rule_fragments = count($rule_fragments); + $count_fragments = count($fragments); + $extractions = array(); + + // If this is a strict matcher, then do a quick test based on fragments. + if ($is_strict && $count_rule_fragments != $count_fragments) + continue; + + // Loop over the pieces of the rule, matching the fragments to that of + // the request. + foreach ($rule_fragments as $i => $rule_frag) { + // Don't iterate past the length of the request. Prefix matching means + // that this can still be a match. + if ($i >= $count_fragments) + break; + + // If this fragment is a key to be extracted, do so into a temporary + // array. + if (strlen($rule_frag) && $rule_frag[0] == '{' && substr($rule_frag, -1) == '}') { + $key = substr($rule_frag, 1, -1); + $extractions[$key] = $fragments[$i]; + } + // Otherwise, the path components mutch match. + else if ($rule_frag != $fragments[$i]) { + $is_match = FALSE; + break; + } + } + + // If no match was made, try the next rule. + if (!$is_match) + continue; + + // A match was made, so merge the path components that were extracted by + // key and return the match. + return [ + 'result' => $action, + 'data' => $extractions, + ]; + } + } + + return NULL; + } +} diff --git a/http2/router.php b/http2/router.php new file mode 100644 index 0000000..d991add --- /dev/null +++ b/http2/router.php @@ -0,0 +1,71 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/pipeline.php'; +require_once HOPLITE_ROOT . '/http2/route_map.php'; + +/*! + The Router is a middleware that operates on a RouteMap object. If a Request + matches, the value in the RouteMap is treated as a Middleware to be built by + Pipeline::buildMiddleware(). + + If the secondary Pipeline Router::subpipe is provided, the matched value will + be treated as if it were Pipeline::add()ed immediately before being built. + This allows additional middleware to execute IFF a route match occurs. + + If no route mach is made, then the Router runs the next middleware. +*/ +class Router extends Middleware { + private $map; + private $subpipe; + + public function __construct(Pipeline $pipeline, + Middleware $next, + RouteMap $map, + Pipeline $subpipe=NULL) { + parent::__construct($pipeline, $next); + $this->map = $map; + $this->subpipe = $subpipe ? $subpipe : new Pipeline(); + } + + public function execute(Request $request) { + // The query rewriter module of the webserver rewrites a request from: + // http://example.com/webapp/user/view/42 + // to: + // http://example.com/webapp/index.php/user/view/42 + // ... which then becomes accessible from PATH_INFO. + if (isset($_SERVER['PATH_INFO'])) + $url = $_SERVER['PATH_INFO']; + else + $url = '/'; + if ($url[0] == '/') + $url = substr($url, 1); + + $request->url = $url; + $route_result = $this->map->match($request); + + if ($route_result) { + $request->context[self::class] = $route_result; + $next = $this->subpipe->buildWithTail($route_result['result']); + return $next->execute($request); + } + + return $this->next->execute($request); + } +} -- 2.22.5