Commit | Line | Data |
---|---|---|
44d399bc S |
1 | <?php |
2 | /** | |
3 | * Zend Framework (http://framework.zend.com/) | |
4 | * | |
5 | * @link http://github.com/zendframework/zf2 for the canonical source repository | |
6 | * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) | |
7 | * @license http://framework.zend.com/license/new-bsd New BSD License | |
8 | */ | |
9 | ||
10 | namespace Zend\Mvc\Router\Http; | |
11 | ||
12 | use Traversable; | |
13 | use Zend\Mvc\Router\Exception; | |
14 | use Zend\Stdlib\ArrayUtils; | |
15 | use Zend\Stdlib\RequestInterface as Request; | |
16 | ||
17 | /** | |
18 | * Hostname route. | |
19 | */ | |
20 | class Hostname implements RouteInterface | |
21 | { | |
22 | /** | |
23 | * Parts of the route. | |
24 | * | |
25 | * @var array | |
26 | */ | |
27 | protected $parts; | |
28 | ||
29 | /** | |
30 | * Regex used for matching the route. | |
31 | * | |
32 | * @var string | |
33 | */ | |
34 | protected $regex; | |
35 | ||
36 | /** | |
37 | * Map from regex groups to parameter names. | |
38 | * | |
39 | * @var array | |
40 | */ | |
41 | protected $paramMap = []; | |
42 | ||
43 | /** | |
44 | * Default values. | |
45 | * | |
46 | * @var array | |
47 | */ | |
48 | protected $defaults; | |
49 | ||
50 | /** | |
51 | * List of assembled parameters. | |
52 | * | |
53 | * @var array | |
54 | */ | |
55 | protected $assembledParams = []; | |
56 | ||
57 | /** | |
58 | * Create a new hostname route. | |
59 | * | |
60 | * @param string $route | |
61 | * @param array $constraints | |
62 | * @param array $defaults | |
63 | */ | |
64 | public function __construct($route, array $constraints = [], array $defaults = []) | |
65 | { | |
66 | $this->defaults = $defaults; | |
67 | $this->parts = $this->parseRouteDefinition($route); | |
68 | $this->regex = $this->buildRegex($this->parts, $constraints); | |
69 | } | |
70 | ||
71 | /** | |
72 | * factory(): defined by RouteInterface interface. | |
73 | * | |
74 | * @see \Zend\Mvc\Router\RouteInterface::factory() | |
75 | * @param array|Traversable $options | |
76 | * @return Hostname | |
77 | * @throws Exception\InvalidArgumentException | |
78 | */ | |
79 | public static function factory($options = []) | |
80 | { | |
81 | if ($options instanceof Traversable) { | |
82 | $options = ArrayUtils::iteratorToArray($options); | |
83 | } elseif (!is_array($options)) { | |
84 | throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options'); | |
85 | } | |
86 | ||
87 | if (!isset($options['route'])) { | |
88 | throw new Exception\InvalidArgumentException('Missing "route" in options array'); | |
89 | } | |
90 | ||
91 | if (!isset($options['constraints'])) { | |
92 | $options['constraints'] = []; | |
93 | } | |
94 | ||
95 | if (!isset($options['defaults'])) { | |
96 | $options['defaults'] = []; | |
97 | } | |
98 | ||
99 | return new static($options['route'], $options['constraints'], $options['defaults']); | |
100 | } | |
101 | ||
102 | /** | |
103 | * Parse a route definition. | |
104 | * | |
105 | * @param string $def | |
106 | * @return array | |
107 | * @throws Exception\RuntimeException | |
108 | */ | |
109 | protected function parseRouteDefinition($def) | |
110 | { | |
111 | $currentPos = 0; | |
112 | $length = strlen($def); | |
113 | $parts = []; | |
114 | $levelParts = [&$parts]; | |
115 | $level = 0; | |
116 | ||
117 | while ($currentPos < $length) { | |
118 | if (!preg_match('(\G(?P<literal>[a-z0-9-.]*)(?P<token>[:{\[\]]|$))', $def, $matches, 0, $currentPos)) { | |
119 | throw new Exception\RuntimeException('Matched hostname literal contains a disallowed character'); | |
120 | } | |
121 | ||
122 | $currentPos += strlen($matches[0]); | |
123 | ||
124 | if (!empty($matches['literal'])) { | |
125 | $levelParts[$level][] = ['literal', $matches['literal']]; | |
126 | } | |
127 | ||
128 | if ($matches['token'] === ':') { | |
129 | if (!preg_match('(\G(?P<name>[^:.{\[\]]+)(?:{(?P<delimiters>[^}]+)})?:?)', $def, $matches, 0, $currentPos)) { | |
130 | throw new Exception\RuntimeException('Found empty parameter name'); | |
131 | } | |
132 | ||
133 | $levelParts[$level][] = ['parameter', $matches['name'], isset($matches['delimiters']) ? $matches['delimiters'] : null]; | |
134 | ||
135 | $currentPos += strlen($matches[0]); | |
136 | } elseif ($matches['token'] === '[') { | |
137 | $levelParts[$level][] = ['optional', []]; | |
138 | $levelParts[$level + 1] = &$levelParts[$level][count($levelParts[$level]) - 1][1]; | |
139 | ||
140 | $level++; | |
141 | } elseif ($matches['token'] === ']') { | |
142 | unset($levelParts[$level]); | |
143 | $level--; | |
144 | ||
145 | if ($level < 0) { | |
146 | throw new Exception\RuntimeException('Found closing bracket without matching opening bracket'); | |
147 | } | |
148 | } else { | |
149 | break; | |
150 | } | |
151 | } | |
152 | ||
153 | if ($level > 0) { | |
154 | throw new Exception\RuntimeException('Found unbalanced brackets'); | |
155 | } | |
156 | ||
157 | return $parts; | |
158 | } | |
159 | ||
160 | /** | |
161 | * Build the matching regex from parsed parts. | |
162 | * | |
163 | * @param array $parts | |
164 | * @param array $constraints | |
165 | * @param int $groupIndex | |
166 | * @return string | |
167 | * @throws Exception\RuntimeException | |
168 | */ | |
169 | protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) | |
170 | { | |
171 | $regex = ''; | |
172 | ||
173 | foreach ($parts as $part) { | |
174 | switch ($part[0]) { | |
175 | case 'literal': | |
176 | $regex .= preg_quote($part[1]); | |
177 | break; | |
178 | ||
179 | case 'parameter': | |
180 | $groupName = '?P<param' . $groupIndex . '>'; | |
181 | ||
182 | if (isset($constraints[$part[1]])) { | |
183 | $regex .= '(' . $groupName . $constraints[$part[1]] . ')'; | |
184 | } elseif ($part[2] === null) { | |
185 | $regex .= '(' . $groupName . '[^.]+)'; | |
186 | } else { | |
187 | $regex .= '(' . $groupName . '[^' . $part[2] . ']+)'; | |
188 | } | |
189 | ||
190 | $this->paramMap['param' . $groupIndex++] = $part[1]; | |
191 | break; | |
192 | ||
193 | case 'optional': | |
194 | $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?'; | |
195 | break; | |
196 | } | |
197 | } | |
198 | ||
199 | return $regex; | |
200 | } | |
201 | ||
202 | /** | |
203 | * Build host. | |
204 | * | |
205 | * @param array $parts | |
206 | * @param array $mergedParams | |
207 | * @param bool $isOptional | |
208 | * @return string | |
209 | * @throws Exception\RuntimeException | |
210 | * @throws Exception\InvalidArgumentException | |
211 | */ | |
212 | protected function buildHost(array $parts, array $mergedParams, $isOptional) | |
213 | { | |
214 | $host = ''; | |
215 | $skip = true; | |
216 | $skippable = false; | |
217 | ||
218 | foreach ($parts as $part) { | |
219 | switch ($part[0]) { | |
220 | case 'literal': | |
221 | $host .= $part[1]; | |
222 | break; | |
223 | ||
224 | case 'parameter': | |
225 | $skippable = true; | |
226 | ||
227 | if (!isset($mergedParams[$part[1]])) { | |
228 | if (!$isOptional) { | |
229 | throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); | |
230 | } | |
231 | ||
232 | return ''; | |
233 | } elseif (!$isOptional || !isset($this->defaults[$part[1]]) || $this->defaults[$part[1]] !== $mergedParams[$part[1]]) { | |
234 | $skip = false; | |
235 | } | |
236 | ||
237 | $host .= $mergedParams[$part[1]]; | |
238 | ||
239 | $this->assembledParams[] = $part[1]; | |
240 | break; | |
241 | ||
242 | case 'optional': | |
243 | $skippable = true; | |
244 | $optionalPart = $this->buildHost($part[1], $mergedParams, true); | |
245 | ||
246 | if ($optionalPart !== '') { | |
247 | $host .= $optionalPart; | |
248 | $skip = false; | |
249 | } | |
250 | break; | |
251 | } | |
252 | } | |
253 | ||
254 | if ($isOptional && $skippable && $skip) { | |
255 | return ''; | |
256 | } | |
257 | ||
258 | return $host; | |
259 | } | |
260 | ||
261 | /** | |
262 | * match(): defined by RouteInterface interface. | |
263 | * | |
264 | * @see \Zend\Mvc\Router\RouteInterface::match() | |
265 | * @param Request $request | |
266 | * @return RouteMatch|null | |
267 | */ | |
268 | public function match(Request $request) | |
269 | { | |
270 | if (!method_exists($request, 'getUri')) { | |
271 | return; | |
272 | } | |
273 | ||
274 | $uri = $request->getUri(); | |
275 | $host = $uri->getHost(); | |
276 | ||
277 | $result = preg_match('(^' . $this->regex . '$)', $host, $matches); | |
278 | ||
279 | if (!$result) { | |
280 | return; | |
281 | } | |
282 | ||
283 | $params = []; | |
284 | ||
285 | foreach ($this->paramMap as $index => $name) { | |
286 | if (isset($matches[$index]) && $matches[$index] !== '') { | |
287 | $params[$name] = $matches[$index]; | |
288 | } | |
289 | } | |
290 | ||
291 | return new RouteMatch(array_merge($this->defaults, $params)); | |
292 | } | |
293 | ||
294 | /** | |
295 | * assemble(): Defined by RouteInterface interface. | |
296 | * | |
297 | * @see \Zend\Mvc\Router\RouteInterface::assemble() | |
298 | * @param array $params | |
299 | * @param array $options | |
300 | * @return mixed | |
301 | */ | |
302 | public function assemble(array $params = [], array $options = []) | |
303 | { | |
304 | $this->assembledParams = []; | |
305 | ||
306 | if (isset($options['uri'])) { | |
307 | $host = $this->buildHost( | |
308 | $this->parts, | |
309 | array_merge($this->defaults, $params), | |
310 | false | |
311 | ); | |
312 | ||
313 | $options['uri']->setHost($host); | |
314 | } | |
315 | ||
316 | // A hostname does not contribute to the path, thus nothing is returned. | |
317 | return ''; | |
318 | } | |
319 | ||
320 | /** | |
321 | * getAssembledParams(): defined by RouteInterface interface. | |
322 | * | |
323 | * @see RouteInterface::getAssembledParams | |
324 | * @return array | |
325 | */ | |
326 | public function getAssembledParams() | |
327 | { | |
328 | return $this->assembledParams; | |
329 | } | |
330 | } |