Implement UrlMap::Evaluate() with all its tests passing.
[hoplite.git] / http / url_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\http;
18
19 /*!
20 A UrlMap will translate a raw HTTP request into a http\Request object. It does
21 so by matching the incoming URL to a pattern. For information on the format of
22 patterns @see ::set_map().
23 */
24 class UrlMap
25 {
26 /*! @var RootController */
27 private $controller;
28
29 /*! @var array The map of URLs to actions. */
30 private $map = array();
31
32 /*!
33 Constructs the object with a reference to the RootController.
34 @param RootController
35 */
36 public function __construct(RootController $controller)
37 {
38 $this->controller = $controller;
39 }
40
41 /*! Accessor for the RootController */
42 public function controller() { return $this->controller; }
43
44 /*! Gets the URL map */
45 public function map() { return $this->map; }
46 /*! @brief Sets the URL map.
47 The URL map is an associative array of URL prefix patterns to Actions or
48 file paths containing actions.
49
50 The keys can be either a URL prefix or a regular expression.
51
52 For URL prefixes, the pattern is matched relative to the root of the entry-
53 point as specified by the RootController. Patterns should not begin with a
54 slash. Path fragment parameter extraction can be performed as well. For
55 example, the pattern 'user/view/{id}' will match a URL like
56 http://example.com/webapp/user/view/42
57 And the http\Request's data will have a member called 'id' with value 42.
58
59 Typically prefix matching will be done for the above patterns. This may be
60 unwanted if a more general path needs to execute before a more descendent
61 one. Using strict matching, invoked by ending a pattern with two slashes,
62 pattern matching becomes exact in that all path components must match.
63
64 Regular expression matching is not limited to prefix patterns and can match
65 any part of the URL (though prefix matching can be enforced using the
66 the standard regex '^' character). Regex patterns must begin and end with
67 '/' characters. During evaluation, if any pattern groups are matched, the
68 resulting matches will be placed in Request data via the 'url_pattern' key.
69 The following will match the same URL as above:
70 '/^user\/view\/([0-9]+)/'
71
72 Values can be a class name or a relative path to a file that contains an
73 Action class by the same name, or just the name of an Action class. The
74 conventions for each are governed by ::LookupAction().
75
76 @see ::Evaluate()
77 @see ::LookupAction()
78 */
79 public function set_map(array $map) { $this->map = $map; }
80
81 /*! @brief Evalutes the URL map and finds a match.
82 This will take the incoming URL from the request and will match it against
83 the patterns in the internal map.
84
85 Matching occurs in a linear scan of the URL map, so precedence can be
86 enforced by ordering rules appropriately. Regular expressions are matched
87 and URL parameter extraction occurs each time a different rule is evaluated.
88
89 This may mutate the request with extracted data if a match is made and
90 returned.
91 @see ::set_map() for more information.
92
93 If a match is made, this will return the corresponding value for the matched
94 key in the map. To get an Action object from this, use ::LookupAction().
95
96 @return string|NULL A matched value in the ::map() or NULL if no match.
97 */
98 public function Evaluate(Request $request)
99 {
100 $fragments = explode('/', $request->url);
101 $path_length = strlen($request->url);
102
103 foreach ($this->map as $rule => $action) {
104 // Check if this is a regular expression rule and match it.
105 if ($rule[0] == '/' && substr($rule, -1) == '/') {
106 $matches = array();
107 if (preg_match($rule, $request->url, $matches)) {
108 // This pattern matched, so fill out the request and return.
109 $request->data['url_pattern'] = $matches;
110 return $action;
111 }
112 }
113 // Otherwise, this is just a normal string match.
114 else {
115 // Patterns that end with two slashes are exact.
116 $is_strict = substr($rule, -2) == '//';
117 if ($is_strict)
118 $rule = substr($rule, 0, -2);
119
120 // Set up some variables for the loop.
121 $is_match = TRUE;
122 $rule_fragments = explode('/', $rule);
123 $count_rule_fragments = count($rule_fragments);
124 $count_fragments = count($fragments);
125 $extractions = array();
126
127 // If this is a strict matcher, then do a quick test based on fragments.
128 if ($is_strict && $count_rule_fragments != $count_fragments)
129 continue;
130
131 // Loop over the pieces of the rule, matching the fragments to that of
132 // the request.
133 foreach ($rule_fragments as $i => $rule_frag) {
134 // Don't iterate past the length of the request. Prefix matching means
135 // that this can still be a match.
136 if ($i >= $count_fragments)
137 break;
138
139 // If this fragment is a key to be extracted, do so into a temporary
140 // array.
141 if ($rule_frag[0] == '{' && substr($rule_frag, -1) == '}') {
142 $key = substr($rule_frag, 1, -1);
143 $extractions[$key] = $fragments[$i];
144 }
145 // Otherwise, the path components mutch match.
146 else if ($rule_frag != $fragments[$i]) {
147 $is_match = FALSE;
148 break;
149 }
150 }
151
152 // If no match was made, try the next rule.
153 if (!$is_match)
154 continue;
155
156 // A match was made, so merge the path components that were extracted by
157 // key and return the match.
158 $request->data = array_merge($extractions, $request->data);
159 return $action;
160 }
161 }
162 }
163
164 /*! @brief Takes a value from the map and returns an Action object.
165 The values in the map are either an Action class name or a relative path to
166 a file containing an Action class.
167
168 Mapping to a class requires that the value start with an upper-case letter.
169
170 Paths start with a lower-case letter. The last path component will be
171 transformed into a class name via ::_ClassNameFromFileName(). Note that if
172 the file extension is not included in the path, .php will be automatically
173 appended.
174
175 @return Action|NULL The loaded action, or NULL on error.
176 */
177 public function LookupAction($map_value)
178 {}
179
180 /*!
181 Takes a file name and returns the name of an Action class. This uses an
182 under_score to CamelCase transformation with an 'Action' suffix:
183 lost_password -> LostPasswordAction
184
185 This can be overridden to provide a custom transformation.
186
187 @return string Action class name.
188 */
189 protected function _ClassNameFromFileName($file_name)
190 {}
191 }