PageRenderTime 65ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/components/com_router/registry.php

https://bitbucket.org/oligriffiths/com_router
PHP | 469 lines | 230 code | 72 blank | 167 comment | 46 complexity | 06a2d2acf6c146a3ab7dcb7ca7a1d7be MD5 | raw file
  1. <?php
  2. /**
  3. * User: Oli Griffiths
  4. * Date: 14/10/2012
  5. * Time: 18:13
  6. */
  7. class ComRouterRegistry extends KObject
  8. {
  9. /**
  10. * Holds the added route configurations
  11. * @note Routes loaded from $this->loadRoutes() are cached in APC, so must be cleared if the files are changed
  12. * @var array
  13. */
  14. protected static $_configurations = array();
  15. /**
  16. * Optional scope whilst reading in routing files, sets the options param
  17. * @var
  18. */
  19. protected static $_scope;
  20. /**
  21. * if set to true, routes for components missing a router or routes.json/php files will be auto created
  22. * @see $this->loadRoutes()
  23. * @var bool
  24. */
  25. protected $_auto_create_routes;
  26. /**
  27. * Cache identifier used by APC
  28. * APC is used to cache 3 things:
  29. * Routes loaded from loadRoutes()
  30. * Parsed routes
  31. * Built routes
  32. *
  33. * If any routes are changed without clearing the cache, APC must be cleaned
  34. * @var string
  35. */
  36. protected $_cache_identifier = 'koowa-route-cache-template';
  37. /**
  38. * Object constructor
  39. * @param KConfig $config
  40. */
  41. public function __construct(KConfig $config = null)
  42. {
  43. parent::__construct($config);
  44. $this->_auto_create_routes = $config->auto_create;
  45. if($config->auto_load) $this->loadRoutes();
  46. }
  47. /**
  48. * Initializes the config
  49. * @param KConfig $config
  50. */
  51. protected function _initialize(KConfig $config)
  52. {
  53. $config->append(array(
  54. 'auto_create' => true,
  55. 'auto_load' => true
  56. ));
  57. parent::_initialize($config);
  58. }
  59. /**
  60. * Main router connect method for adding routes
  61. * @param $rule - the rule itself, see ComRouterRoute::$_template
  62. * @param array $params
  63. * @param array $config
  64. * @see ComRouterRoute::$_template
  65. * @see ComRouterRoute::$_params
  66. * @see http://lithify.me/docs/lithium/net/http/Route
  67. * @return mixed
  68. */
  69. public function connect($rule, $params = array(), $config = array())
  70. {
  71. $params = $params instanceof KConfig ? $params->toArray() : $params;
  72. $config = $config instanceof KConfig ? $config->toArray() : $config;
  73. //Set the scope if defined
  74. if(self::$_scope && !isset($params['option'])) $params['option'] = self::$_scope;
  75. //Add starting slash
  76. $rule = '/'.ltrim($rule,'/');
  77. //Set the config
  78. $config['template'] = $rule;
  79. $config['params'] = $params;
  80. //Get route instance
  81. self::$_configurations[$rule] = KService::get('com://site/router.route', $config);
  82. return self::$_configurations[$rule];
  83. }
  84. /**
  85. * Removes a connected route if it exists
  86. * @param $rule
  87. */
  88. public function disconnect($rule)
  89. {
  90. unset(self::$_configurations[$rule]);
  91. }
  92. /**
  93. * Returns the defined routes
  94. * @return array
  95. */
  96. public function routes()
  97. {
  98. return self::$_configurations;
  99. }
  100. /**
  101. * Parses a url and returns a matching internal route or false on no match
  102. * @param $url
  103. * @return bool|KHttpUrl
  104. */
  105. public function parse(KHttpUrl $url)
  106. {
  107. $url = clone $url; //Ensure original url is not modified
  108. $path = $url->getUrl(KHttpUrl::PATH);
  109. $cache_id = $this->_cache_identifier.'-parse-'.$path;
  110. if(extension_loaded('apc'))
  111. {
  112. $vars = apc_fetch($cache_id);
  113. if($vars && count($vars)){
  114. $url->query = $vars;
  115. $url->path = 'index';
  116. $url->format = 'php';
  117. return $url;
  118. }
  119. }
  120. $routes = array();
  121. //Check for an exact match first
  122. if(isset(self::$_configurations[$path])){
  123. $routes[] = self::$_configurations[$path];
  124. }else{
  125. //If an exact match failed, attempt to match as many routes based on the route path as possible
  126. //For example /products/{:id} would be matched against /products/5.html, (/products = /products)
  127. //This reduces the number of routes that have to be parsed below, = more efficient
  128. //Attempt to match the base route
  129. $parts = explode('/',ltrim($path,'/'));
  130. //Compile a list of paths for simple matching
  131. $path = '';
  132. foreach($parts AS $part)
  133. {
  134. $path .= '/'.$part;
  135. foreach(array_keys(array_reverse(self::$_configurations, true)) AS $route){
  136. if(preg_match('#^'.preg_quote($path,'#').'#', $route)){
  137. $routes[$route] = self::$_configurations[$route];
  138. }
  139. }
  140. }
  141. }
  142. //If we have no matched routes, get all routes
  143. if(empty($routes)){
  144. $routes = self::$_configurations;
  145. }
  146. //Loop routes and attempt to match
  147. $vars = array();
  148. $count = count($routes)-1;
  149. $i = 0;
  150. foreach($routes AS $route)
  151. {
  152. $tmp_url = clone $url;
  153. if($route_vars = $route->parse($tmp_url)){
  154. $vars = $vars + $route_vars;
  155. if(!$route->canContinue() || $count == $i) break;
  156. }
  157. $i++;
  158. }
  159. //If we have vars, return the new url
  160. if(count($vars)){
  161. $url->query = $vars;
  162. $url->path = 'index';
  163. $url->format = 'php';
  164. if(extension_loaded('apc'))
  165. {
  166. apc_store($cache_id, $vars);
  167. }
  168. return $url;
  169. }
  170. return false;
  171. }
  172. /**
  173. * Attempts to match a KHttpUrl against the defined routes a connected `Route` object.
  174. * For example, given the following route:
  175. *
  176. * {{{
  177. * Router::connect('/login', array('option' => 'com_users', 'view' => 'login'));
  178. * }}}
  179. *
  180. * This will match:
  181. * {{{
  182. * $url = Router::match(array('option' => 'com_users', 'view' => 'login'));
  183. * // returns /login
  184. * }}}
  185. *
  186. * For URLs templates with no insert parameters (i.e. elements like `{:id}` that are replaced
  187. * with a value), all parameters must match exactly as they appear in the route parameters.
  188. *
  189. * Alternatively to using a full array, you can specify routes using a more compact syntax. The
  190. * above example can be written as:
  191. *
  192. * {{{ $url = Router::match('Users::login'); // still returns /login }}}
  193. *
  194. * You can combine this with more complicated routes; for example:
  195. * {{{
  196. * Router::connect('/posts/{:id:\d+}', array('controller' => 'posts', 'action' => 'view'));
  197. * }}}
  198. *
  199. * This will match:
  200. * {{{
  201. * $url = Router::match(array('controller' => 'posts', 'action' => 'view', 'id' => '1138'));
  202. * // returns /posts/1138
  203. * }}}
  204. *
  205. * Again, you can specify the same URL with a more compact syntax, as in the following:
  206. * {{{
  207. * $url = Router::match(array('Posts::view', 'id' => '1138'));
  208. * // again, returns /posts/1138
  209. * }}}
  210. *
  211. * You can use either syntax anywhere a URL is accepted, i.e.
  212. * `lithium\action\Controller::redirect()`, or `lithium\template\helper\Html::link()`.
  213. *
  214. * @param string|array $url Options to match to a URL. Optionally, this can be a string
  215. * containing a manually generated URL.
  216. * @param object $context An instance of `lithium\action\Request`. This supplies the context for
  217. * any persistent parameters, as well as the base URL for the application.
  218. * @param array $options Options for the generation of the matched URL. Currently accepted
  219. * values are:
  220. * - `'absolute'` _boolean_: Indicates whether or not the returned URL should be an
  221. * absolute path (i.e. including scheme and host name).
  222. * - `'host'` _string_: If `'absolute'` is `true`, sets the host name to be used,
  223. * or overrides the one provided in `$context`.
  224. * - `'scheme'` _string_: If `'absolute'` is `true`, sets the URL scheme to be
  225. * used, or overrides the one provided in `$context`.
  226. * @return string Returns a generated URL, based on the URL template of the matched route, and
  227. * prefixed with the base URL of the application.
  228. */
  229. public function match(KHttpUrl $url, $context = null, array $options = array())
  230. {
  231. $query = $url->query;
  232. ksort($query);
  233. $url->query = $query;
  234. $cache_id = $this->_cache_identifier.'-match-'.http_build_query($query);
  235. if(extension_loaded('apc') && 0)
  236. {
  237. $parts = apc_fetch($cache_id);
  238. if($parts && count($parts)){
  239. $url->query = $parts['query'];
  240. $url->path = $parts['path'];
  241. $url->format = $parts['format'];
  242. return true;
  243. }
  244. }
  245. $match_query_count = count($url->query);
  246. $match = null;
  247. $query = array();
  248. foreach (static::$_configurations as $route) {
  249. $tmp_url = clone $url;
  250. if (!$route->match($tmp_url, $context)) {
  251. continue;
  252. }
  253. /**
  254. * Validate how much of the querystring the route matched, and if it matched more than a previous route
  255. * This is done because we're matching only querystring variables, and routes may be defined in any order
  256. * We need to find the most specific route, eg:
  257. * /shop <= option=com_shop
  258. * /shop/products =< option=com_shop&view=products
  259. *
  260. * /shop will be matched first for the url index.php?option=com_shop&view=products, but we have another route
  261. * that is more specific, the 2nd route. Thus we need to see how much of each route removes from the querstring
  262. * in an attempt to find the best match route
  263. **/
  264. if(count(array_intersect_key($tmp_url->query, $url->query)) < $match_query_count){
  265. $match_query_count = count($tmp_url->query);
  266. $match = $tmp_url;
  267. //If the route is a fallthrough route, extract its querystring parameters
  268. if($route->canContinue()){
  269. $query = empty($query) ? array_diff_key($url->query, $match->query) : array_diff_key($match->query, $query);
  270. }else{
  271. $query = array_diff_key($match->query, $url->query);
  272. }
  273. }
  274. }
  275. //If we found a match, set the url to the match
  276. if($match){
  277. $url->path = is_array($match->path) ? array_filter($match->path) : explode('/',ltrim($match->path,'/'));
  278. $url->query = $match->query + $query;
  279. $url->format = isset($url->query['format']) ? $url->query['format'] : 'html';
  280. unset($url->query['format']);
  281. if(extension_loaded('apc'))
  282. {
  283. apc_store($cache_id, array('path' => $url->path, 'query' => $url->query, 'format' => $url->format));
  284. }
  285. return true;
  286. }else{
  287. return false;
  288. }
  289. }
  290. /**
  291. * Route loader searches the components directory in all com_ folders looking for a
  292. * routes.php or routes.json to include or parse and add routes
  293. */
  294. public function loadRoutes()
  295. {
  296. static $loaded;
  297. if(extension_loaded('apc')){
  298. if($routes = apc_fetch($this->_cache_identifier.'-routes')){
  299. self::$_configurations = $routes;
  300. $loaded = true;
  301. return;
  302. }
  303. }
  304. if(!$loaded){
  305. //Nooku server
  306. $components = glob(JPATH_ROOT.'/site/components/com_*', GLOB_ONLYDIR);
  307. //Joomla BC
  308. if(!$components) $components = glob(JPATH_ROOT.'/components/com_*', GLOB_ONLYDIR);
  309. //Find all components and look for routes
  310. if($components){
  311. foreach($components AS $dir){
  312. $component = basename($dir);
  313. //Skip core components
  314. if(in_array($component, array('com_application','com_default','com_router'))) continue;
  315. //Skip components with a router, these are handling routing themselves
  316. if(!file_exists($dir.'/router.php')){
  317. //Set the scope for this component
  318. self::$_scope = $component;
  319. if($files = glob($dir.'/routes.{php,json}',GLOB_BRACE)){
  320. foreach($files AS $file)
  321. {
  322. if(preg_match('#\.php$#', $file)){
  323. //Include the router
  324. include_once $file;
  325. }else{
  326. //Read the file and process the params
  327. $content = file_get_contents($file);
  328. $json = json_decode($content, true);
  329. if($json){
  330. foreach($json AS $route => $options)
  331. {
  332. //Setup options
  333. if(!is_array($options)){
  334. parse_str($options, $params);
  335. }else if(isset($options['params'])){
  336. $params = $options['params'];
  337. unset($options['params']);
  338. }else{
  339. $params = $options;
  340. }
  341. //Connect the route
  342. self::connect($route, $params, $options);
  343. }
  344. }
  345. }
  346. //Reset the scope
  347. self::$_scope = null;
  348. }
  349. }else{
  350. /**
  351. * Register some default routes.
  352. * 1 of 2 routes are registered:
  353. * component/views <- plural views
  354. * component/view/id <- singular views
  355. **/
  356. $component = substr($component, 4);
  357. $views = glob($dir.'/views/*', GLOB_ONLYDIR);
  358. //Connect the base component route
  359. $this->connect($component);
  360. //Connect views
  361. foreach($views AS &$view){
  362. $view = basename($view);
  363. if($view == $component){
  364. $this->connect($component, array('view' => $view));
  365. }else{
  366. $singular = KInflector::isSingular($view);
  367. if($singular){
  368. $this->connect($component.'/'.$view.'/{:id:\d*}', array('view' => $view, 'id' => null));
  369. }else{
  370. $this->connect($component.'/'.$view, array('view' => $view));
  371. }
  372. }
  373. $layouts = glob($dir.'/views/'.$view.'/tmpl/*.php');
  374. foreach($layouts AS $layout){
  375. $layout = substr(basename($layout), 0, -4);
  376. //Ignore partials and default
  377. if($layout != 'default' && !preg_match('#^_#',$layout) && !preg_match('#^default_#',$layout) && !preg_match('#^form_#',$layout)){
  378. if($singular){
  379. $this->connect($component.'/'.$view.'/{:id:\d+}/'.$layout, array('view' => $view, 'layout' => $layout));
  380. $this->connect($component.'/'.$view.'/'.$layout, array('view' => $view, 'layout' => $layout));
  381. }else{
  382. $this->connect($component.'/'.$view.'/'.$layout, array('view' => $view, 'layout' => $layout));
  383. }
  384. }
  385. }
  386. }
  387. }
  388. }
  389. }
  390. if(extension_loaded('apc')){
  391. apc_store($this->_cache_identifier.'-routes', self::$_configurations);
  392. }
  393. }
  394. }
  395. return $this;
  396. }
  397. }