/components/com_router/registry.php
PHP | 469 lines | 230 code | 72 blank | 167 comment | 46 complexity | 06a2d2acf6c146a3ab7dcb7ca7a1d7be MD5 | raw file
- <?php
- /**
- * User: Oli Griffiths
- * Date: 14/10/2012
- * Time: 18:13
- */
- class ComRouterRegistry extends KObject
- {
- /**
- * Holds the added route configurations
- * @note Routes loaded from $this->loadRoutes() are cached in APC, so must be cleared if the files are changed
- * @var array
- */
- protected static $_configurations = array();
- /**
- * Optional scope whilst reading in routing files, sets the options param
- * @var
- */
- protected static $_scope;
- /**
- * if set to true, routes for components missing a router or routes.json/php files will be auto created
- * @see $this->loadRoutes()
- * @var bool
- */
- protected $_auto_create_routes;
- /**
- * Cache identifier used by APC
- * APC is used to cache 3 things:
- * Routes loaded from loadRoutes()
- * Parsed routes
- * Built routes
- *
- * If any routes are changed without clearing the cache, APC must be cleaned
- * @var string
- */
- protected $_cache_identifier = 'koowa-route-cache-template';
- /**
- * Object constructor
- * @param KConfig $config
- */
- public function __construct(KConfig $config = null)
- {
- parent::__construct($config);
- $this->_auto_create_routes = $config->auto_create;
- if($config->auto_load) $this->loadRoutes();
- }
- /**
- * Initializes the config
- * @param KConfig $config
- */
- protected function _initialize(KConfig $config)
- {
- $config->append(array(
- 'auto_create' => true,
- 'auto_load' => true
- ));
- parent::_initialize($config);
- }
- /**
- * Main router connect method for adding routes
- * @param $rule - the rule itself, see ComRouterRoute::$_template
- * @param array $params
- * @param array $config
- * @see ComRouterRoute::$_template
- * @see ComRouterRoute::$_params
- * @see http://lithify.me/docs/lithium/net/http/Route
- * @return mixed
- */
- public function connect($rule, $params = array(), $config = array())
- {
- $params = $params instanceof KConfig ? $params->toArray() : $params;
- $config = $config instanceof KConfig ? $config->toArray() : $config;
- //Set the scope if defined
- if(self::$_scope && !isset($params['option'])) $params['option'] = self::$_scope;
- //Add starting slash
- $rule = '/'.ltrim($rule,'/');
- //Set the config
- $config['template'] = $rule;
- $config['params'] = $params;
- //Get route instance
- self::$_configurations[$rule] = KService::get('com://site/router.route', $config);
- return self::$_configurations[$rule];
- }
- /**
- * Removes a connected route if it exists
- * @param $rule
- */
- public function disconnect($rule)
- {
- unset(self::$_configurations[$rule]);
- }
- /**
- * Returns the defined routes
- * @return array
- */
- public function routes()
- {
- return self::$_configurations;
- }
- /**
- * Parses a url and returns a matching internal route or false on no match
- * @param $url
- * @return bool|KHttpUrl
- */
- public function parse(KHttpUrl $url)
- {
- $url = clone $url; //Ensure original url is not modified
- $path = $url->getUrl(KHttpUrl::PATH);
- $cache_id = $this->_cache_identifier.'-parse-'.$path;
- if(extension_loaded('apc'))
- {
- $vars = apc_fetch($cache_id);
- if($vars && count($vars)){
- $url->query = $vars;
- $url->path = 'index';
- $url->format = 'php';
- return $url;
- }
- }
- $routes = array();
- //Check for an exact match first
- if(isset(self::$_configurations[$path])){
- $routes[] = self::$_configurations[$path];
- }else{
- //If an exact match failed, attempt to match as many routes based on the route path as possible
- //For example /products/{:id} would be matched against /products/5.html, (/products = /products)
- //This reduces the number of routes that have to be parsed below, = more efficient
- //Attempt to match the base route
- $parts = explode('/',ltrim($path,'/'));
- //Compile a list of paths for simple matching
- $path = '';
- foreach($parts AS $part)
- {
- $path .= '/'.$part;
- foreach(array_keys(array_reverse(self::$_configurations, true)) AS $route){
- if(preg_match('#^'.preg_quote($path,'#').'#', $route)){
- $routes[$route] = self::$_configurations[$route];
- }
- }
- }
- }
- //If we have no matched routes, get all routes
- if(empty($routes)){
- $routes = self::$_configurations;
- }
- //Loop routes and attempt to match
- $vars = array();
- $count = count($routes)-1;
- $i = 0;
- foreach($routes AS $route)
- {
- $tmp_url = clone $url;
- if($route_vars = $route->parse($tmp_url)){
- $vars = $vars + $route_vars;
- if(!$route->canContinue() || $count == $i) break;
- }
- $i++;
- }
- //If we have vars, return the new url
- if(count($vars)){
- $url->query = $vars;
- $url->path = 'index';
- $url->format = 'php';
- if(extension_loaded('apc'))
- {
- apc_store($cache_id, $vars);
- }
- return $url;
- }
- return false;
- }
- /**
- * Attempts to match a KHttpUrl against the defined routes a connected `Route` object.
- * For example, given the following route:
- *
- * {{{
- * Router::connect('/login', array('option' => 'com_users', 'view' => 'login'));
- * }}}
- *
- * This will match:
- * {{{
- * $url = Router::match(array('option' => 'com_users', 'view' => 'login'));
- * // returns /login
- * }}}
- *
- * For URLs templates with no insert parameters (i.e. elements like `{:id}` that are replaced
- * with a value), all parameters must match exactly as they appear in the route parameters.
- *
- * Alternatively to using a full array, you can specify routes using a more compact syntax. The
- * above example can be written as:
- *
- * {{{ $url = Router::match('Users::login'); // still returns /login }}}
- *
- * You can combine this with more complicated routes; for example:
- * {{{
- * Router::connect('/posts/{:id:\d+}', array('controller' => 'posts', 'action' => 'view'));
- * }}}
- *
- * This will match:
- * {{{
- * $url = Router::match(array('controller' => 'posts', 'action' => 'view', 'id' => '1138'));
- * // returns /posts/1138
- * }}}
- *
- * Again, you can specify the same URL with a more compact syntax, as in the following:
- * {{{
- * $url = Router::match(array('Posts::view', 'id' => '1138'));
- * // again, returns /posts/1138
- * }}}
- *
- * You can use either syntax anywhere a URL is accepted, i.e.
- * `lithium\action\Controller::redirect()`, or `lithium\template\helper\Html::link()`.
- *
- * @param string|array $url Options to match to a URL. Optionally, this can be a string
- * containing a manually generated URL.
- * @param object $context An instance of `lithium\action\Request`. This supplies the context for
- * any persistent parameters, as well as the base URL for the application.
- * @param array $options Options for the generation of the matched URL. Currently accepted
- * values are:
- * - `'absolute'` _boolean_: Indicates whether or not the returned URL should be an
- * absolute path (i.e. including scheme and host name).
- * - `'host'` _string_: If `'absolute'` is `true`, sets the host name to be used,
- * or overrides the one provided in `$context`.
- * - `'scheme'` _string_: If `'absolute'` is `true`, sets the URL scheme to be
- * used, or overrides the one provided in `$context`.
- * @return string Returns a generated URL, based on the URL template of the matched route, and
- * prefixed with the base URL of the application.
- */
- public function match(KHttpUrl $url, $context = null, array $options = array())
- {
- $query = $url->query;
- ksort($query);
- $url->query = $query;
- $cache_id = $this->_cache_identifier.'-match-'.http_build_query($query);
- if(extension_loaded('apc') && 0)
- {
- $parts = apc_fetch($cache_id);
- if($parts && count($parts)){
- $url->query = $parts['query'];
- $url->path = $parts['path'];
- $url->format = $parts['format'];
- return true;
- }
- }
- $match_query_count = count($url->query);
- $match = null;
- $query = array();
- foreach (static::$_configurations as $route) {
- $tmp_url = clone $url;
- if (!$route->match($tmp_url, $context)) {
- continue;
- }
- /**
- * Validate how much of the querystring the route matched, and if it matched more than a previous route
- * This is done because we're matching only querystring variables, and routes may be defined in any order
- * We need to find the most specific route, eg:
- * /shop <= option=com_shop
- * /shop/products =< option=com_shop&view=products
- *
- * /shop will be matched first for the url index.php?option=com_shop&view=products, but we have another route
- * that is more specific, the 2nd route. Thus we need to see how much of each route removes from the querstring
- * in an attempt to find the best match route
- **/
- if(count(array_intersect_key($tmp_url->query, $url->query)) < $match_query_count){
- $match_query_count = count($tmp_url->query);
- $match = $tmp_url;
- //If the route is a fallthrough route, extract its querystring parameters
- if($route->canContinue()){
- $query = empty($query) ? array_diff_key($url->query, $match->query) : array_diff_key($match->query, $query);
- }else{
- $query = array_diff_key($match->query, $url->query);
- }
- }
- }
- //If we found a match, set the url to the match
- if($match){
- $url->path = is_array($match->path) ? array_filter($match->path) : explode('/',ltrim($match->path,'/'));
- $url->query = $match->query + $query;
- $url->format = isset($url->query['format']) ? $url->query['format'] : 'html';
- unset($url->query['format']);
- if(extension_loaded('apc'))
- {
- apc_store($cache_id, array('path' => $url->path, 'query' => $url->query, 'format' => $url->format));
- }
- return true;
- }else{
- return false;
- }
- }
- /**
- * Route loader searches the components directory in all com_ folders looking for a
- * routes.php or routes.json to include or parse and add routes
- */
- public function loadRoutes()
- {
- static $loaded;
- if(extension_loaded('apc')){
- if($routes = apc_fetch($this->_cache_identifier.'-routes')){
- self::$_configurations = $routes;
- $loaded = true;
- return;
- }
- }
- if(!$loaded){
- //Nooku server
- $components = glob(JPATH_ROOT.'/site/components/com_*', GLOB_ONLYDIR);
- //Joomla BC
- if(!$components) $components = glob(JPATH_ROOT.'/components/com_*', GLOB_ONLYDIR);
- //Find all components and look for routes
- if($components){
- foreach($components AS $dir){
- $component = basename($dir);
- //Skip core components
- if(in_array($component, array('com_application','com_default','com_router'))) continue;
- //Skip components with a router, these are handling routing themselves
- if(!file_exists($dir.'/router.php')){
- //Set the scope for this component
- self::$_scope = $component;
- if($files = glob($dir.'/routes.{php,json}',GLOB_BRACE)){
- foreach($files AS $file)
- {
- if(preg_match('#\.php$#', $file)){
- //Include the router
- include_once $file;
- }else{
- //Read the file and process the params
- $content = file_get_contents($file);
- $json = json_decode($content, true);
- if($json){
- foreach($json AS $route => $options)
- {
- //Setup options
- if(!is_array($options)){
- parse_str($options, $params);
- }else if(isset($options['params'])){
- $params = $options['params'];
- unset($options['params']);
- }else{
- $params = $options;
- }
- //Connect the route
- self::connect($route, $params, $options);
- }
- }
- }
- //Reset the scope
- self::$_scope = null;
- }
- }else{
- /**
- * Register some default routes.
- * 1 of 2 routes are registered:
- * component/views <- plural views
- * component/view/id <- singular views
- **/
- $component = substr($component, 4);
- $views = glob($dir.'/views/*', GLOB_ONLYDIR);
- //Connect the base component route
- $this->connect($component);
- //Connect views
- foreach($views AS &$view){
- $view = basename($view);
- if($view == $component){
- $this->connect($component, array('view' => $view));
- }else{
- $singular = KInflector::isSingular($view);
- if($singular){
- $this->connect($component.'/'.$view.'/{:id:\d*}', array('view' => $view, 'id' => null));
- }else{
- $this->connect($component.'/'.$view, array('view' => $view));
- }
- }
- $layouts = glob($dir.'/views/'.$view.'/tmpl/*.php');
- foreach($layouts AS $layout){
- $layout = substr(basename($layout), 0, -4);
- //Ignore partials and default
- if($layout != 'default' && !preg_match('#^_#',$layout) && !preg_match('#^default_#',$layout) && !preg_match('#^form_#',$layout)){
- if($singular){
- $this->connect($component.'/'.$view.'/{:id:\d+}/'.$layout, array('view' => $view, 'layout' => $layout));
- $this->connect($component.'/'.$view.'/'.$layout, array('view' => $view, 'layout' => $layout));
- }else{
- $this->connect($component.'/'.$view.'/'.$layout, array('view' => $view, 'layout' => $layout));
- }
- }
- }
- }
- }
- }
- }
- if(extension_loaded('apc')){
- apc_store($this->_cache_identifier.'-routes', self::$_configurations);
- }
- }
- }
- return $this;
- }
- }