1: <?php
2: /**
3: * Slim - a micro PHP 5 framework
4: *
5: * @author Josh Lockhart <info@slimframework.com>
6: * @copyright 2011 Josh Lockhart
7: * @link http://www.slimframework.com
8: * @license http://www.slimframework.com/license
9: * @version 2.0.0
10: * @package Slim
11: *
12: * MIT LICENSE
13: *
14: * Permission is hereby granted, free of charge, to any person obtaining
15: * a copy of this software and associated documentation files (the
16: * "Software"), to deal in the Software without restriction, including
17: * without limitation the rights to use, copy, modify, merge, publish,
18: * distribute, sublicense, and/or sell copies of the Software, and to
19: * permit persons to whom the Software is furnished to do so, subject to
20: * the following conditions:
21: *
22: * The above copyright notice and this permission notice shall be
23: * included in all copies or substantial portions of the Software.
24: *
25: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26: * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27: * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28: * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29: * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30: * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31: * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32: */
33: namespace Slim;
34:
35: /**
36: * Route
37: * @package Slim
38: * @author Josh Lockhart, Thomas Bley
39: * @since 1.0.0
40: */
41: class Route
42: {
43: /**
44: * @var string The route pattern (e.g. "/books/:id")
45: */
46: protected $pattern;
47:
48: /**
49: * @var mixed The route callable
50: */
51: protected $callable;
52:
53: /**
54: * @var array Conditions for this route's URL parameters
55: */
56: protected $conditions = array();
57:
58: /**
59: * @var array Default conditions applied to all route instances
60: */
61: protected static $defaultConditions = array();
62:
63: /**
64: * @var string The name of this route (optional)
65: */
66: protected $name;
67:
68: /**
69: * @var array Key-value array of URL parameters
70: */
71: protected $params = array();
72:
73: /**
74: * @var array value array of URL parameter names
75: */
76: protected $paramNames = array();
77:
78: /**
79: * @var array key array of URL parameter names with + at the end
80: */
81: protected $paramNamesPath = array();
82:
83: /**
84: * @var array HTTP methods supported by this Route
85: */
86: protected $methods = array();
87:
88: /**
89: * @var array[Callable] Middleware to be run before only this route instance
90: */
91: protected $middleware = array();
92:
93: /**
94: * Constructor
95: * @param string $pattern The URL pattern (e.g. "/books/:id")
96: * @param mixed $callable Anything that returns TRUE for is_callable()
97: */
98: public function __construct($pattern, $callable)
99: {
100: $this->setPattern($pattern);
101: $this->setCallable($callable);
102: $this->setConditions(self::getDefaultConditions());
103: }
104:
105: /**
106: * Set default route conditions for all instances
107: * @param array $defaultConditions
108: */
109: public static function setDefaultConditions(array $defaultConditions)
110: {
111: self::$defaultConditions = $defaultConditions;
112: }
113:
114: /**
115: * Get default route conditions for all instances
116: * @return array
117: */
118: public static function getDefaultConditions()
119: {
120: return self::$defaultConditions;
121: }
122:
123: /**
124: * Get route pattern
125: * @return string
126: */
127: public function getPattern()
128: {
129: return $this->pattern;
130: }
131:
132: /**
133: * Set route pattern
134: * @param string $pattern
135: */
136: public function setPattern($pattern)
137: {
138: $this->pattern = $pattern;
139: }
140:
141: /**
142: * Get route callable
143: * @return mixed
144: */
145: public function getCallable()
146: {
147: return $this->callable;
148: }
149:
150: /**
151: * Set route callable
152: * @param mixed $callable
153: * @throws \InvalidArgumentException If argument is not callable
154: */
155: public function setCallable($callable)
156: {
157: if (!is_callable($callable)) {
158: throw new \InvalidArgumentException('Route callable must be callable');
159: }
160:
161: $this->callable = $callable;
162: }
163:
164: /**
165: * Get route conditions
166: * @return array
167: */
168: public function getConditions()
169: {
170: return $this->conditions;
171: }
172:
173: /**
174: * Set route conditions
175: * @param array $conditions
176: */
177: public function setConditions(array $conditions)
178: {
179: $this->conditions = $conditions;
180: }
181:
182: /**
183: * Get route name
184: * @return string|null
185: */
186: public function getName()
187: {
188: return $this->name;
189: }
190:
191: /**
192: * Set route name
193: * @param string $name
194: */
195: public function setName($name)
196: {
197: $this->name = (string) $name;
198: }
199:
200: /**
201: * Get route parameters
202: * @return array
203: */
204: public function getParams()
205: {
206: return $this->params;
207: }
208:
209: /**
210: * Set route parameters
211: * @param array $params
212: */
213: public function setParams($params)
214: {
215: $this->params = $params;
216: }
217:
218: /**
219: * Get route parameter value
220: * @param string $index Name of URL parameter
221: * @return string
222: * @throws \InvalidArgumentException If route parameter does not exist at index
223: */
224: public function getParam($index)
225: {
226: if (!isset($this->params[$index])) {
227: throw new \InvalidArgumentException('Route parameter does not exist at specified index');
228: }
229:
230: return $this->params[$index];
231: }
232:
233: /**
234: * Set route parameter value
235: * @param string $index Name of URL parameter
236: * @param mixed $value The new parameter value
237: * @throws \InvalidArgumentException If route parameter does not exist at index
238: */
239: public function setParam($index, $value)
240: {
241: if (!isset($this->params[$index])) {
242: throw new \InvalidArgumentException('Route parameter does not exist at specified index');
243: }
244: $this->params[$index] = $value;
245: }
246:
247: /**
248: * Add supported HTTP method(s)
249: */
250: public function setHttpMethods()
251: {
252: $args = func_get_args();
253: $this->methods = $args;
254: }
255:
256: /**
257: * Get supported HTTP methods
258: * @return array
259: */
260: public function getHttpMethods()
261: {
262: return $this->methods;
263: }
264:
265: /**
266: * Append supported HTTP methods
267: */
268: public function appendHttpMethods()
269: {
270: $args = func_get_args();
271: $this->methods = array_merge($this->methods, $args);
272: }
273:
274: /**
275: * Append supported HTTP methods (alias for Route::appendHttpMethods)
276: * @return \Slim\Route
277: */
278: public function via()
279: {
280: $args = func_get_args();
281: $this->methods = array_merge($this->methods, $args);
282:
283: return $this;
284: }
285:
286: /**
287: * Detect support for an HTTP method
288: * @return bool
289: */
290: public function supportsHttpMethod($method)
291: {
292: return in_array($method, $this->methods);
293: }
294:
295: /**
296: * Get middleware
297: * @return array[Callable]
298: */
299: public function getMiddleware()
300: {
301: return $this->middleware;
302: }
303:
304: /**
305: * Set middleware
306: *
307: * This method allows middleware to be assigned to a specific Route.
308: * If the method argument `is_callable` (including callable arrays!),
309: * we directly append the argument to `$this->middleware`. Else, we
310: * assume the argument is an array of callables and merge the array
311: * with `$this->middleware`. Each middleware is checked for is_callable()
312: * and an InvalidArgumentException is thrown immediately if it isn't.
313: *
314: * @param Callable|array[Callable]
315: * @return \Slim\Route
316: * @throws \InvalidArgumentException If argument is not callable or not an array of callables.
317: */
318: public function setMiddleware($middleware)
319: {
320: if (is_callable($middleware)) {
321: $this->middleware[] = $middleware;
322: } elseif (is_array($middleware)) {
323: foreach($middleware as $callable) {
324: if (!is_callable($callable)) {
325: throw new \InvalidArgumentException('All Route middleware must be callable');
326: }
327: }
328: $this->middleware = array_merge($this->middleware, $middleware);
329: } else {
330: throw new \InvalidArgumentException('Route middleware must be callable or an array of callables');
331: }
332:
333: return $this;
334: }
335:
336: /**
337: * Matches URI?
338: *
339: * Parse this route's pattern, and then compare it to an HTTP resource URI
340: * This method was modeled after the techniques demonstrated by Dan Sosedoff at:
341: *
342: * http://blog.sosedoff.com/2009/09/20/rails-like-php-url-router/
343: *
344: * @param string $resourceUri A Request URI
345: * @return bool
346: */
347: public function matches($resourceUri)
348: {
349: //Convert URL params into regex patterns, construct a regex for this route, init params
350: $patternAsRegex = preg_replace_callback('#:([\w]+)\+?#', array($this, 'matchesCallback'),
351: str_replace(')', ')?', (string) $this->pattern));
352: if (substr($this->pattern, -1) === '/') {
353: $patternAsRegex .= '?';
354: }
355:
356: //Cache URL params' names and values if this route matches the current HTTP request
357: if (!preg_match('#^' . $patternAsRegex . '$#', $resourceUri, $paramValues)) {
358: return false;
359: }
360: foreach ($this->paramNames as $name) {
361: if (isset($paramValues[$name])) {
362: if (isset($this->paramNamesPath[ $name ])) {
363: $this->params[$name] = explode('/', urldecode($paramValues[$name]));
364: } else {
365: $this->params[$name] = urldecode($paramValues[$name]);
366: }
367: }
368: }
369:
370: return true;
371: }
372:
373: /**
374: * Convert a URL parameter (e.g. ":id", ":id+") into a regular expression
375: * @param array URL parameters
376: * @return string Regular expression for URL parameter
377: */
378: protected function matchesCallback($m)
379: {
380: $this->paramNames[] = $m[1];
381: if (isset($this->conditions[ $m[1] ])) {
382: return '(?P<' . $m[1] . '>' . $this->conditions[ $m[1] ] . ')';
383: }
384: if (substr($m[0], -1) === '+') {
385: $this->paramNamesPath[ $m[1] ] = 1;
386:
387: return '(?P<' . $m[1] . '>.+)';
388: }
389:
390: return '(?P<' . $m[1] . '>[^/]+)';
391: }
392:
393: /**
394: * Set route name
395: * @param string $name The name of the route
396: * @return \Slim\Route
397: */
398: public function name($name)
399: {
400: $this->setName($name);
401:
402: return $this;
403: }
404:
405: /**
406: * Merge route conditions
407: * @param array $conditions Key-value array of URL parameter conditions
408: * @return \Slim\Route
409: */
410: public function conditions(array $conditions)
411: {
412: $this->conditions = array_merge($this->conditions, $conditions);
413:
414: return $this;
415: }
416: }
417: