Middleware no longer takes a Pipeline.
[hoplite.git] / http2 / route_map.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2011 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\http2;
18
19 /*!
20 A RouteMap parses a HTTP Request URL, matches it against its set of rules, and
21 finds one that matches. If it is successful, data are extracted from the URL,
22 based on the specified pattern.
23
24 The map is an associative array of URL prefix patterns to a result value.
25 When used with the Router Middleware, the value should be a Middleware that
26 can be built by Pipeline::buildMiddleware(). An Action subclass typical.
27
28 The keys can be either a URL prefix or a regular expression.
29
30 For URL prefixes, the pattern is matched relative to the Request::url field.
31 Patterns should not begin with a slash. Path fragment parameter extraction
32 can be performed as well. For example, the pattern 'user/view/{id}' will
33 match a URL like
34 http://example.com/webapp/user/view/42
35 And the match result data will have a key called 'id' with value 42.
36
37 Typically prefix matching will be done for the above patterns. This may be
38 unwanted if a more general path needs to execute before a more descendent
39 one. Using strict matching, invoked by ending a pattern with two slashes,
40 pattern matching becomes exact in that all path components must match.
41
42 Regular expression matching is not limited to prefix patterns and can match
43 any part of the URL (though prefix matching can be enforced using the
44 the standard regex '^' character). Regex patterns must begin and end with
45 '/' characters. During evaluation, if any pattern groups are matched, the
46 resulting matches will be placed in Request data via the 'url_pattern' key.
47 The following will match the same URL as above:
48 '/^user\/view\/([0-9]+)/'
49 */
50 class RouteMap {
51 /*! @var array The map of URLs to actions. */
52 private $map;
53
54 public function __construct(array $map) {
55 $this->map = $map;
56 }
57
58 /*! @brief Evalutes the URL map and finds a match.
59 This will take the incoming URL from the request and will match it against
60 the patterns in the internal map.
61
62 Matching occurs in a linear scan of the URL map, so precedence can be
63 enforced by ordering rules appropriately. Regular expressions are matched
64 and URL parameter extraction occurs each time a different rule is evaluated.
65
66 If a match is made, an associative array with this stucture is returned:
67 [
68 'result' => <value in map>,
69 'data' => [ <associative-array url data> ],
70 'regexp' => <preg_match result>
71 ]
72
73 @return array|NULL A matched value in the ::map or NULL if no match.
74 */
75 public function match(Request $request) {
76 $fragments = explode('/', $request->url);
77 $path_length = strlen($request->url);
78
79 $result = [];
80
81 foreach ($this->map as $rule => $action) {
82 // First check if this is the empty rule.
83 if (empty($rule)) {
84 if (empty($request->url))
85 return [ 'result' => $action ];
86 else
87 continue;
88 }
89
90 // Check if this is a regular expression rule and match it.
91 if ($rule[0] == '/' && substr($rule, -1) == '/') {
92 $matches = array();
93 if (preg_match($rule, $request->url, $matches)) {
94 // This pattern matched, so fill out the request and return.
95 return [
96 'result' => $action,
97 'regexp' => $matches,
98 ];
99 }
100 }
101 // Otherwise, this is just a normal string match.
102 else {
103 // Patterns that end with two slashes are exact.
104 $is_strict = substr($rule, -2) == '//';
105 if ($is_strict)
106 $rule = substr($rule, 0, -2);
107
108 // Set up some variables for the loop.
109 $is_match = TRUE;
110 $rule_fragments = explode('/', $rule);
111 $count_rule_fragments = count($rule_fragments);
112 $count_fragments = count($fragments);
113 $extractions = array();
114
115 // If this is a strict matcher, then do a quick test based on fragments.
116 if ($is_strict && $count_rule_fragments != $count_fragments)
117 continue;
118
119 // Loop over the pieces of the rule, matching the fragments to that of
120 // the request.
121 foreach ($rule_fragments as $i => $rule_frag) {
122 // Don't iterate past the length of the request. Prefix matching means
123 // that this can still be a match.
124 if ($i >= $count_fragments)
125 break;
126
127 // If this fragment is a key to be extracted, do so into a temporary
128 // array.
129 if (strlen($rule_frag) && $rule_frag[0] == '{' && substr($rule_frag, -1) == '}') {
130 $key = substr($rule_frag, 1, -1);
131 $extractions[$key] = $fragments[$i];
132 }
133 // Otherwise, the path components mutch match.
134 else if ($rule_frag != $fragments[$i]) {
135 $is_match = FALSE;
136 break;
137 }
138 }
139
140 // If no match was made, try the next rule.
141 if (!$is_match)
142 continue;
143
144 // A match was made, so merge the path components that were extracted by
145 // key and return the match.
146 return [
147 'result' => $action,
148 'data' => $extractions,
149 ];
150 }
151 }
152
153 return NULL;
154 }
155 }