-- improved support in params in the route
[limb:trunk.git] / web_app / src / request / lmbRoutes.class.php
1 <?php
2 /*
3  * Limb PHP Framework
4  *
5  * @link http://limb-project.com
6  * @copyright  Copyright &copy; 2004-2009 BIT(http://bit-creative.com)
7  * @license    LGPL http://www.gnu.org/copyleft/lesser.html
8  */
9
10 /**
11  * class lmbRoutes.
12  *
13  * @package web_app
14  * @version $Id$
15  */
16 class lmbRoutes
17 {
18   protected $config = array();
19   const NAMED_PARAM_REGEXP = '(?:\/([^\/]+))?';
20   const EXTRA_PARAM_REGEXP = '(?:\/(.*))?';
21
22   function __construct($config)
23   {
24     $this->config = $config;
25   }
26
27   function dispatch($url)
28   {
29     foreach($this->config as $route)
30     {
31       if(($result = $this->_getResultMatchedParams($route, $url)) === null)
32         continue;
33
34       if(!$this->_routeParamsMeetRequirements($route, $result))
35         continue;
36
37       return $this->_applyDispatchFilter($route, $result);
38     }
39
40     return array();
41   }
42
43   function toUrl($params, $route_name = '')
44   {
45     if($route_name && isset($this->config[$route_name]))
46     {
47       if($path = $this->_makeUrlByRoute($params, $this->config[$route_name]))
48         return $path;
49     }
50     elseif(!$route_name)
51     {
52       foreach($this->config as $name => $route)
53       {
54         if($path = $this->_makeUrlByRoute($params, $route))
55           return $path;
56       }
57     }
58     throw new lmbException($message = "Route '$route_name' not found for params '" . lmb_var_dump($params) . "'");
59   }
60
61   protected function _applyDispatchFilter($route, $dispatched)
62   {
63     if(!isset($route['dispatch_filter']) && !isset($route['rewriter']))
64       return $dispatched;
65
66     //'rewriter' is going to be obsolete
67     $filter = isset($route['dispatch_filter']) ? $route['dispatch_filter'] : $route['rewriter'];
68
69     if(!is_callable($filter))
70       throw new lmbException('Dispatch filter is not callable!', array('filter' => $filter));
71
72     call_user_func_array($filter, array(&$dispatched, $route));
73     return $dispatched;
74   }
75
76   protected function _applyUrlFilter($route, $path)
77   {
78     if(!isset($route['url_filter']))
79       return $path;
80
81     $filter = $route['url_filter'];
82
83     if(!is_callable($filter))
84       throw new lmbException('Url filter is not callable!', array('filter' => $filter));
85
86     call_user_func_array($filter, array(&$path, $route));
87     return $path;
88   }
89
90   protected function _getResultMatchedParams($route, $url)
91   {
92     if(($matched_params = $this->_getMatchedParams($route, $url)) === null)
93       return null;
94
95     if(isset($route['defaults']))
96       return array_merge($route['defaults'], $matched_params);
97     else
98       return $matched_params;
99   }
100
101   function _getMatchedParams($route, $url)
102   {
103     $named_params = array();
104
105     $regexp = $this->_getRouteRegexp($route['path'], $named_params);
106
107     if(!preg_match($regexp, $url, $matched_params))
108       return null;
109
110     if (array_filter($matched_params)!=$matched_params)
111       return null;
112
113     array_shift($matched_params);
114
115     $result = array();
116
117     $index = 0;
118     foreach($matched_params as $matched_item)
119       if($param_name = $named_params[$index++])
120         $result[$param_name] = urldecode($matched_item);
121
122     return $result;
123   }
124
125   function _getRouteRegexp($route_path, &$named_params)
126   {
127     $elements = array();
128     foreach (explode('/', $route_path) as $element)
129       if (trim($element))
130         $elements[] = $element;
131
132     $final_regexp_parts = array();
133
134     foreach ($elements as $element)
135     {
136       if($name = $this->_getNamedUrlParam($element))
137       {
138         $final_regexp_parts[] = '(?:\/'. preg_replace('/:'. $name .':?/', '([^\/]+)', $element). ')?';
139         $named_params[] = $name;
140       }
141       elseif ($name = $this->_getExtraNamedParam($element))
142       {
143         $final_regexp_parts[] = self :: EXTRA_PARAM_REGEXP;
144         $named_params[] = $name;
145       }
146       else
147         $final_regexp_parts[] = '/' . $element;
148     }
149
150     return '#^' . implode('', $final_regexp_parts) . '[\/]*$#';
151   }
152
153   protected function _getNamedUrlParam($element)
154   {
155     if(preg_match('/^[^:]*:([^:]+):?.*$/', $element, $matches))
156       return $matches[1];
157     else
158       return null;
159   }
160
161   protected function _getExtraNamedParam($element)
162   {
163     if(preg_match('/^\*(.+)?$/', $element, $matches))
164     {
165       if(isset($matches[1]))
166         return $matches[1];
167       else
168         return 'extra';
169     }
170     else
171       return null;
172   }
173
174   protected function _routeParamsMeetRequirements($route, $params)
175   {
176     foreach($params as $param_name => $param_value)
177     {
178       if(!$this->_singleParamMeetsRequirements($route, $param_name, $param_value))
179         return false;
180     }
181     return true;
182   }
183
184   protected function _singleParamMeetsRequirements($route, $param_name, $param_value)
185   {
186      return (!isset($route['requirements'][$param_name]) ||
187             preg_match($route['requirements'][$param_name], $param_value, $req_res));
188   }
189
190   function _makeUrlByRoute($params, $route)
191   {
192     $path = $route['path'];
193
194     if(!$this->_routeParamsMeetRequirements($route, $params))
195     {
196       return "";
197     }
198
199     foreach($params as $param_name => $param_value)
200     {
201       if (isset($route['defaults'][$param_name]) && ($route['defaults'][$param_name] === $param_value)) {
202         unset($params[$param_name]); // default params will be substituted lower
203         continue;
204       }
205
206       if(strpos($path, ':'.$param_name) === false)
207         continue;
208
209       $path = preg_replace('/\:'. preg_quote($param_name) .'\:?/', $param_value, $path);
210       unset($params[$param_name]);
211     }
212
213     if(count($params))
214       return '';
215
216     if(isset($route['defaults']))
217     {
218       // we define here required default params for building right url,
219       // other params at the end of the path can be omitted.
220       $required_params = array();
221       if (preg_match_all('|(:\w+/?)+(?=/\w+)|', $path, $matched_params))
222       {
223         foreach($matched_params[0] as $param)
224         {
225           $required_params = array_merge(explode('/', $param), $required_params);
226         }
227       }
228
229       foreach($route['defaults'] as $param_name => $param_value)
230       {
231         if(!in_array(':' . $param_name, $required_params))
232           $param_value = '';
233
234         $path = str_replace(':' . $param_name, $param_value, $path);
235       }
236
237       $path = preg_replace('~/+~', '/', $path);
238     }
239
240     if(strpos($path, "/:") !== false)
241       return '';
242
243     return $this->_applyUrlFilter($route, $path);
244   }
245 }
246
247