Files
server/usr/share/psa-pear/pear/php/Horde/Routes/Mapper.php
2026-01-07 20:52:11 +01:00

1202 lines
41 KiB
PHP

<?php
/**
* Horde Routes package
*
* This package is heavily inspired by the Python "Routes" library
* by Ben Bangert (http://routes.groovie.org). Routes is based
* largely on ideas from Ruby on Rails (http://www.rubyonrails.org).
*
* @author Maintainable Software, LLC. (http://www.maintainable.com)
* @author Mike Naberezny <mike@maintainable.com>
* @license http://www.horde.org/licenses/bsd BSD
* @package Routes
*/
/**
* The mapper class handles URL generation and recognition for web applications
*
* The mapper class is built by handling associated arrays of information and passing
* associated arrays back to the application for it to handle and dispatch the
* appropriate scripts.
*
* @package Routes
*/
class Horde_Routes_Mapper
{
/**
* Filtered request environment with keys like SCRIPT_NAME
* @var array
*/
public $environ = array();
/**
* Callback function used to get array of controller names
* @var callback
*/
public $controllerScan;
/**
* Path to controller directory passed to controllerScan function
* @var string
*/
public $directory;
/**
* Call controllerScan callback before every route match?
* @var boolean
*/
public $alwaysScan;
/**
* Disable route memory and implicit defaults?
* @var boolean
*/
public $explicit;
/**
* Collect debug information during route match?
* @var boolean
*/
public $debug = false;
/**
* Use sub-domain support?
* @var boolean
*/
public $subDomains = false;
/**
* Array of sub-domains to ignore if using sub-domain support
* @var array
*/
public $subDomainsIgnore = array();
/**
* Append trailing slash ('/') to generated routes?
* @var boolean
*/
public $appendSlash = false;
/**
* Prefix to strip during matching and to append during generation
* @var null|string
*/
public $prefix = null;
/**
* Array of connected routes
* @var array
*/
public $matchList = array();
/**
* Array of connected named routes, indexed by name
* @var array
*/
public $routeNames = array();
/**
* Cache of URLs used in generate()
* @var array
*/
public $urlCache = array();
/**
* Encoding of routes URLs (not yet supported)
* @var string
*/
public $encoding = 'utf-8';
/**
* What to do on decoding errors? 'ignore' or 'replace'
* @var string
*/
public $decodeErrors = 'ignore';
/**
* Partial regexp used to match domain part of the end of URLs to match
* @var string
*/
public $domainMatch = '[^\.\/]+?\.[^\.\/]+';
/**
* Array of all connected routes, indexed by the serialized array of all
* keys that each route could utilize.
* @var array
*/
public $maxKeys = array();
/**
* Array of all connected routes, indexed by the serialized array of the
* minimum keys that each route needs.
* @var array
*/
public $minKeys = array();
/**
* Utility functions like urlFor() and redirectTo() for this Mapper
* @var Horde_Routes_Utils
*/
public $utils;
/**
* Cache
* @var Horde_Cache
*/
public $cache;
/**
* Cache lifetime for the same value of $this->matchList
* @var integer
*/
public $cacheLifetime = 86400;
/**
* Have regular expressions been created for all connected routes?
* @var boolean
*/
protected $_createdRegs = false;
/**
* Have generation hashes been created for all connected routes?
* @var boolean
*/
protected $_createdGens = false;
/**
* Generation hashes created for all connected routes
* @var array
*/
protected $_gendict;
/**
* Temporary variable used to pass array of keys into _keysort() callback
* @var array
*/
protected $_keysortTmp;
/**
* Regular expression generated to match after the prefix
* @var string
*/
protected $_regPrefix = null;
/**
* Constructor.
*
* Keyword arguments ($kargs):
* ``controllerScan`` (callback)
* Function to return an array of valid controllers
*
* ``redirect`` (callback)
* Function to perform a redirect for Horde_Routes_Utils->redirectTo()
*
* ``directory`` (string)
* Path to the directory that will be passed to the
* controllerScan callback
*
* ``alwaysScan`` (boolean)
* Should the controllerScan callback be called
* before every URL match?
*
* ``explicit`` (boolean)
* Should routes be connected with the implicit defaults of
* array('controller'=>'content', 'action'=>'index', 'id'=>null)?
* When set to True, these will not be added to route connections.
*/
public function __construct($kargs = array())
{
$callback = array('Horde_Routes_Utils', 'controllerScan');
$defaultKargs = array('controllerScan' => $callback,
'directory' => null,
'alwaysScan' => false,
'explicit' => false);
$kargs = array_merge($defaultKargs, $kargs);
// Most default assignments that were in the construct in the Python
// version have been moved to outside the constructor unless they were variable
$this->directory = $kargs['directory'];
$this->alwaysScan = $kargs['alwaysScan'];
$this->controllerScan = $kargs['controllerScan'];
$this->explicit = $kargs['explicit'];
$this->utils = new Horde_Routes_Utils($this);
}
/**
* Create and connect a new Route to the Mapper.
*
* Usage:
* $m = new Horde_Routes_Mapper();
* $m->connect(':controller/:action/:id');
* $m->connect('date/:year/:month/:day', array('controller' => "blog", 'action' => 'view');
* $m->connect('archives/:page', array('controller' => 'blog', 'action' => 'by_page',
* ' requirements' => array('page' => '\d{1,2}')));
* $m->connect('category_list',
* 'archives/category/:section', array('controller' => 'blog', 'action' => 'category',
* 'section' => 'home', 'type' => 'list'));
* $m->connect('home',
* '',
* array('controller' => 'blog', 'action' => 'view', 'section' => 'home'));
*
* @param mixed $first First argument in vargs, see usage above.
* @param mixed $second Second argument in varags
* @param mixed $third Third argument in varargs
* @return void
*/
public function connect($first, $second = null, $third = null)
{
if ($third !== null) {
// 3 args given
// connect('route_name', ':/controller/:action/:id', array('kargs'=>'here'))
$routeName = $first;
$routePath = $second;
$kargs = $third;
} else if ($second !== null) {
// 2 args given
if (is_array($second)) {
// connect(':/controller/:action/:id', array('kargs'=>'here'))
$routeName = null;
$routePath = $first;
$kargs = $second;
} else {
// connect('route_name', ':/controller/:action/:id')
$routeName = $first;
$routePath = $second;
$kargs = array();
}
} else {
// 1 arg given
// connect('/:controller/:action/:id')
$routeName = null;
$routePath = $first;
$kargs = array();
}
if (!in_array('_explicit', $kargs)) {
$kargs['_explicit'] = $this->explicit;
}
$route = new Horde_Routes_Route($routePath, $kargs);
if ($this->encoding != 'utf-8' || $this->decodeErrors != 'ignore') {
$route->encoding = $this->encoding;
$route->decodeErrors = $this->decodeErrors;
}
$this->matchList[] = $route;
if (isset($routeName)) {
$this->routeNames[$routeName] = $route;
}
if ($route->static) {
return;
}
$exists = false;
foreach ($this->maxKeys as $key => $value) {
if (unserialize($key) == $route->maxKeys) {
$this->maxKeys[$key][] = $route;
$exists = true;
break;
}
}
if (!$exists) {
$this->maxKeys[serialize($route->maxKeys)] = array($route);
}
$this->_createdGens = false;
}
/**
* Set an optional Horde_Cache object for the created rules.
*
* @param Horde_Cache $cache Cache object
*/
public function setCache(Horde_Cache $cache)
{
$this->cache = $cache;
}
/**
* Create the generation hashes (arrays) for route lookups
*
* @return void
*/
protected function _createGens()
{
// Checked for a cached generator dictionary for $this->matchList
if ($this->cache) {
$cacheKey = 'horde.routes.' . sha1(serialize($this->matchList));
$cachedDict = $cache->get($cacheKey, $this->cacheLifetime);
if ($gendict = @unserialize($cachedDict)) {
$this->_gendict = $gendict;
$this->_createdGens = true;
return;
}
}
// Use keys temporarily to assemble the list to avoid excessive
// list iteration testing with foreach. We include the '*' in the
// case that a generate contains a controller/action that has no
// hardcodes.
$actionList = $controllerList = array('*' => true);
// Assemble all the hardcoded/defaulted actions/controllers used
foreach ($this->matchList as $route) {
if ($route->static) {
continue;
}
if (isset($route->defaults['controller'])) {
$controllerList[$route->defaults['controller']] = true;
}
if (isset($route->defaults['action'])) {
$actionList[$route->defaults['action']] = true;
}
}
$actionList = array_keys($actionList);
$controllerList = array_keys($controllerList);
// Go through our list again, assemble the controllers/actions we'll
// add each route to. If its hardcoded, we only add it to that dict key.
// Otherwise we add it to every hardcode since it can be changed.
$gendict = array(); // Our generated two-deep hash
foreach ($this->matchList as $route) {
if ($route->static) {
continue;
}
$clist = $controllerList;
$alist = $actionList;
if (in_array('controller', $route->hardCoded)) {
$clist = array($route->defaults['controller']);
}
if (in_array('action', $route->hardCoded)) {
$alist = array($route->defaults['action']);
}
foreach ($clist as $controller) {
foreach ($alist as $action) {
if (in_array($controller, array_keys($gendict))) {
$actiondict = &$gendict[$controller];
} else {
$gendict[$controller] = array();
$actiondict = &$gendict[$controller];
}
if (in_array($action, array_keys($actiondict))) {
$tmp = $actiondict[$action];
} else {
$tmp = array(array(), array());
}
$tmp[0][] = $route;
$actiondict[$action] = $tmp;
}
}
}
if (!isset($gendict['*'])) {
$gendict['*'] = array();
}
// Write to the cache
if ($this->cache) {
$this->cache->set($cacheKey, serialize($gendict), $this->cacheLifetime);
}
$this->_gendict = $gendict;
$this->_createdGens = true;
}
/**
* Creates the regexes for all connected routes
*
* @param array $clist controller list, controller_scan will be used otherwise
* @return void
*/
public function createRegs($clist = null)
{
if ($clist === null) {
if ($this->directory === null) {
$clist = call_user_func($this->controllerScan);
} else {
$clist = call_user_func($this->controllerScan, $this->directory);
}
}
foreach ($this->maxKeys as $key => $val) {
foreach ($val as $route) {
$route->makeRegexp($clist);
}
}
// Create our regexp to strip the prefix
if (!empty($this->prefix)) {
$this->_regPrefix = $this->prefix . '(.*)';
}
$this->_createdRegs = true;
}
/**
* Internal Route matcher
*
* Matches a URL against a route, and returns a tuple (array) of the
* match dict (array) and the route object if a match is successful,
* otherwise it returns null.
*
* @param string $url URL to match
* @return null|array Match data if matched, otherwise null
*/
protected function _match($url)
{
if (!$this->_createdRegs && !empty($this->controllerScan)) {
$this->createRegs();
} elseif (!$this->_createdRegs) {
$msg = 'You must generate the regular expressions before matching.';
throw new Horde_Routes_Exception($msg);
}
if ($this->alwaysScan) {
$this->createRegs();
}
$matchLog = array();
if (!empty($this->prefix)) {
if (preg_match('@' . $this->_regPrefix . '@', $url)) {
$url = preg_replace('@' . $this->_regPrefix . '@', '$1', $url);
if (empty($url)) {
$url = '/';
}
} else {
return array(null, null, $matchLog);
}
}
foreach ($this->matchList as $route) {
if ($route->static) {
if ($this->debug) {
$matchLog[] = array('route' => $route, 'static' => true);
}
continue;
}
$match = $route->match($url, array('environ' => $this->environ,
'subDomains' => $this->subDomains,
'subDomainsIgnore' => $this->subDomainsIgnore,
'domainMatch' => $this->domainMatch));
if ($this->debug) {
$matchLog[] = array('route' => $route, 'regexp' => (bool)$match);
}
if ($match) {
return array($match, $route, $matchLog);
}
}
return array(null, null, $matchLog);
}
/**
* Match a URL against one of the routes contained.
* It will return null if no valid match is found.
*
* Usage:
* $resultdict = $m->match('/joe/sixpack');
*
* @param string $url URL to match
* @param array|null Array if matched, otherwise null
*/
public function match($url)
{
if (!strlen($url)) {
$msg = 'No URL provided, the minimum URL necessary to match is "/"';
throw new Horde_Routes_Exception($msg);
}
$result = $this->_match($url);
if ($this->debug) {
return array($result[0], $result[1], $result[2]);
}
return ($result[0]) ? $result[0] : null;
}
/**
* Match a URL against one of the routes contained.
* It will return null if no valid match is found, otherwise
* a result dict (array) and a route object is returned.
*
* Usage:
* list($resultdict, $resultobj) = $m->match('/joe/sixpack');
*
* @param string $url URL to match
* @param array|null Array if matched, otherwise null
*/
public function routematch($url)
{
$result = $this->_match($url);
if ($this->debug) {
return array($result[0], $result[1], $result[2]);
}
return ($result[0]) ? array($result[0], $result[1]) : null;
}
/**
* Generates the URL from a given set of keywords
* Returns the URL text, or null if no URL could be generated.
*
* Usage:
* $m->generate(array('controller' => 'content', 'action' => 'view', 'id' => 10));
*
* @param array $routeArgs Optional explicit route list
* @param array $kargs Keyword arguments (key/value pairs)
* @return null|string URL text or null
*/
public function generate($first = null, $second = null)
{
if ($second) {
$routeArgs = $first;
$kargs = is_null($second) ? array() : $second;
} else {
$routeArgs = array();
$kargs = is_null($first) ? array() : $first;
}
// Generate ourself if we haven't already
if (!$this->_createdGens) {
$this->_createGens();
}
if ($this->appendSlash) {
$kargs['_appendSlash'] = true;
}
if (!$this->explicit) {
if (!in_array('controller', array_keys($kargs))) {
$kargs['controller'] = 'content';
}
if (!in_array('action', array_keys($kargs))) {
$kargs['action'] = 'index';
}
}
$environ = $this->environ;
$controller = isset($kargs['controller']) ? $kargs['controller'] : null;
$action = isset($kargs['action']) ? $kargs['action'] : null;
// If the URL didn't depend on the SCRIPT_NAME, we'll cache it
// keyed by just the $kargs; otherwise we need to cache it with
// both SCRIPT_NAME and $kargs:
$cacheKey = serialize($kargs);
if (!empty($environ['SCRIPT_NAME'])) {
$cacheKeyScriptName = sprintf('%s:%s', $environ['SCRIPT_NAME'], $cacheKey);
} else {
$cacheKeyScriptName = $cacheKey;
}
// Check the URL cache to see if it exists, use it if it does.
foreach (array($cacheKey, $cacheKeyScriptName) as $key) {
if (in_array($key, array_keys($this->urlCache))) {
return $this->urlCache[$key];
}
}
if ($routeArgs) {
$keyList = $routeArgs;
} else {
$actionList = isset($this->_gendict[$controller]) ? $this->_gendict[$controller] : $this->_gendict['*'];
list($keyList, $sortCache) =
(isset($actionList[$action])) ? $actionList[$action] : ((isset($actionList['*'])) ? $actionList['*'] : array(null, null));
if ($keyList === null) {
return null;
}
}
$keys = array_keys($kargs);
// necessary to pass $keys to _keysort() callback used by PHP's usort()
$this->_keysortTmp = $keys;
$newList = array();
foreach ($keyList as $route) {
$tmp = Horde_Routes_Utils::arraySubtract($route->minKeys, $keys);
if (count($tmp) == 0) {
$newList[] = $route;
}
}
$keyList = $newList;
// inline python function keysort() moved below as _keycmp()
$this->_keysort($keyList);
foreach ($keyList as $route) {
$fail = false;
foreach ($route->hardCoded as $key) {
$kval = isset($kargs[$key]) ? $kargs[$key] : null;
if ($kval == null) {
continue;
}
if ($kval != $route->defaults[$key]) {
$fail = true;
break;
}
}
if ($fail) {
continue;
}
$path = $route->generate($kargs);
if ($path) {
if ($this->prefix) {
$path = $this->prefix . $path;
}
if (!empty($environ['SCRIPT_NAME']) && !$route->absolute) {
$path = $environ['SCRIPT_NAME'] . $path;
$key = $cacheKeyScriptName;
} else {
$key = $cacheKey;
}
if ($this->urlCache != null) {
$this->urlCache[$key] = $path;
}
return $path;
} else {
continue;
}
}
return null;
}
/**
* Generate routes for a controller resource
*
* The $memberName name should be the appropriate singular version of the
* resource given your locale and used with members of the collection.
*
* The $collectionName name will be used to refer to the resource
* collection methods and should be a plural version of the $memberName
* argument. By default, the $memberName name will also be assumed to map
* to a controller you create.
*
* The concept of a web resource maps somewhat directly to 'CRUD'
* operations. The overlying things to keep in mind is that mapping a
* resource is about handling creating, viewing, and editing that
* resource.
*
* All keyword arguments ($kargs) are optional.
*
* ``controller``
* If specified in the keyword args, the controller will be the actual
* controller used, but the rest of the naming conventions used for
* the route names and URL paths are unchanged.
*
* ``collection``
* Additional action mappings used to manipulate/view the entire set of
* resources provided by the controller.
*
* Example::
*
* $map->resource('message', 'messages',
* array('collection' => array('rss' => 'GET)));
* # GET /message;rss (maps to the rss action)
* # also adds named route "rss_message"
*
* ``member``
* Additional action mappings used to access an individual 'member'
* of this controllers resources.
*
* Example::
*
* $map->resource('message', 'messages',
* array('member' => array('mark' => 'POST')));
* # POST /message/1;mark (maps to the mark action)
* # also adds named route "mark_message"
*
* ``new``
* Action mappings that involve dealing with a new member in the
* controller resources.
*
* Example::
*
* $map->resource('message', 'messages',
* array('new' => array('preview' => 'POST')));
* # POST /message/new;preview (maps to the preview action)
* # also adds a url named "preview_new_message"
*
* ``pathPrefix``
* Prepends the URL path for the Route with the pathPrefix given.
* This is most useful for cases where you want to mix resources
* or relations between resources.
*
* ``namePrefix``
* Perpends the route names that are generated with the namePrefix
* given. Combined with the pathPrefix option, it's easy to
* generate route names and paths that represent resources that are
* in relations.
*
* Example::
*
* map.resource('message', 'messages',
* array('controller' => 'categories',
* 'pathPrefix' => '/category/:category_id',
* 'namePrefix' => 'category_')));
* # GET /category/7/message/1
* # has named route "category_message"
*
* ``parentResource``
* An assoc. array containing information about the parent resource,
* for creating a nested resource. It should contain the ``$memberName``
* and ``collectionName`` of the parent resource. This assoc. array will
* be available via the associated ``Route`` object which can be
* accessed during a request via ``request.environ['routes.route']``
*
* If ``parentResource`` is supplied and ``pathPrefix`` isn't,
* ``pathPrefix`` will be generated from ``parentResource`` as
* "<parent collection name>/:<parent member name>_id".
*
* If ``parentResource`` is supplied and ``namePrefix`` isn't,
* ``namePrefix`` will be generated from ``parentResource`` as
* "<parent member name>_".
*
* Example::
*
* $m = new Horde_Routes_Mapper();
* $utils = $m->utils;
*
* $m->resource('location', 'locations',
* array('parentResource' =>
* array('memberName' => 'region',
* 'collectionName' => 'regions'))));
* # pathPrefix is "regions/:region_id"
* # namePrefix is "region_"
*
* $utils->urlFor('region_locations', array('region_id'=>13));
* # '/regions/13/locations'
*
* $utils->urlFor('region_new_location', array('region_id'=>13));
* # '/regions/13/locations/new'
*
* $utils->urlFor('region_location',
* array('region_id'=>13, 'id'=>60));
* # '/regions/13/locations/60'
*
* $utils->urlFor('region_edit_location',
* array('region_id'=>13, 'id'=>60));
* # '/regions/13/locations/60/edit'
*
* Overriding generated ``pathPrefix``::
*
* $m = new Horde_Routes_Mapper();
* $utils = new Horde_Routes_Utils();
*
* $m->resource('location', 'locations',
* array('parentResource' =>
* array('memberName' => 'region',
* 'collectionName' => 'regions'),
* 'pathPrefix' => 'areas/:area_id')));
* # name prefix is "region_"
*
* $utils->urlFor('region_locations', array('area_id'=>51));
* # '/areas/51/locations'
*
* Overriding generated ``namePrefix``::
*
* $m = new Horde_Routes_Mapper
* $m->resource('location', 'locations',
* array('parentResource' =>
* array('memberName' => 'region',
* 'collectionName' => 'regions'),
* 'namePrefix' => '')));
* # pathPrefix is "regions/:region_id"
*
* $utils->urlFor('locations', array('region_id'=>51));
* # '/regions/51/locations'
*
* Note: Since Horde Routes 0.2.0 and Python Routes 1.8, this method is
* not compatible with earlier versions inasmuch as the semicolon is no
* longer used to delimit custom actions. This was a change in Rails
* itself (http://dev.rubyonrails.org/changeset/6485) and adopting it
* here allows us to keep parity with Rails and ActiveResource.
*
* @param string $memberName Singular version of the resource name
* @param string $collectionName Collection name (plural of $memberName)
* @param array $kargs Keyword arguments (see above)
* @return void
*/
public function resource($memberName, $collectionName, $kargs = array())
{
$defaultKargs = array('collection' => array(),
'member' => array(),
'new' => array(),
'pathPrefix' => null,
'namePrefix' => null,
'parentResource' => null);
$kargs = array_merge($defaultKargs, $kargs);
// Generate ``pathPrefix`` if ``pathPrefix`` wasn't specified and
// ``parentResource`` was. Likewise for ``namePrefix``. Make sure
// that ``pathPrefix`` and ``namePrefix`` *always* take precedence if
// they are specified--in particular, we need to be careful when they
// are explicitly set to "".
if ($kargs['parentResource'] !== null) {
if ($kargs['pathPrefix'] === null) {
$kargs['pathPrefix'] = $kargs['parentResource']['collectionName'] . '/:'
. $kargs['parentResource']['memberName'] . '_id';
}
if ($kargs['namePrefix'] === null) {
$kargs['namePrefix'] = $kargs['parentResource']['memberName'] . '_';
}
} else {
if ($kargs['pathPrefix'] === null) {
$kargs['pathPrefix'] = '';
}
if ($kargs['namePrefix'] === null) {
$kargs['namePrefix'] = '';
}
}
// Ensure the edit and new actions are in and GET
$kargs['member']['edit'] = 'GET';
$kargs['new']['new'] = 'GET';
// inline python method swap() moved below as _swap()
$collectionMethods = $this->_swap($kargs['collection'], array());
$memberMethods = $this->_swap($kargs['member'], array());
$newMethods = $this->_swap($kargs['new'], array());
// Insert create, update, and destroy methods
if (!isset($collectionMethods['POST'])) {
$collectionMethods['POST'] = array();
}
array_unshift($collectionMethods['POST'], 'create');
if (!isset($memberMethods['PUT'])) {
$memberMethods['PUT'] = array();
}
array_unshift($memberMethods['PUT'], 'update');
if (!isset($memberMethods['DELETE'])) {
$memberMethods['DELETE'] = array();
}
array_unshift($memberMethods['DELETE'], 'delete');
// If there's a path prefix option, use it with the controller
$controller = $this->_stripSlashes($collectionName);
$kargs['pathPrefix'] = $this->_stripSlashes($kargs['pathPrefix']);
if ($kargs['pathPrefix']) {
$path = $kargs['pathPrefix'] . '/' . $controller;
} else {
$path = $controller;
}
$collectionPath = $path;
$newPath = $path . '/new';
$memberPath = $path . '/:(id)';
$options = array(
'controller' => (isset($kargs['controller']) ? $kargs['controller'] : $controller),
'_memberName' => $memberName,
'_collectionName' => $collectionName,
'_parentResource' => $kargs['parentResource']
);
// inline python method requirements_for() moved below as _requirementsFor()
// Add the routes for handling collection methods
foreach ($collectionMethods as $method => $lst) {
$primary = ($method != 'GET' && isset($lst[0])) ? array_shift($lst) : null;
$routeOptions = $this->_requirementsFor($method, $options);
foreach ($lst as $action) {
$routeOptions['action'] = $action;
$routeName = sprintf('%s%s_%s', $kargs['namePrefix'], $action, $collectionName);
$this->connect($routeName,
sprintf("%s/%s", $collectionPath, $action),
$routeOptions);
$this->connect('formatted_' . $routeName,
sprintf("%s/%s.:(format)", $collectionPath, $action),
$routeOptions);
}
if ($primary) {
$routeOptions['action'] = $primary;
$this->connect($collectionPath, $routeOptions);
$this->connect($collectionPath . '.:(format)', $routeOptions);
}
}
// Specifically add in the built-in 'index' collection method and its
// formatted version
$connectkargs = array('action' => 'index',
'conditions' => array('method' => array('GET')));
$this->connect($kargs['namePrefix'] . $collectionName,
$collectionPath,
array_merge($connectkargs, $options));
$this->connect('formatted_' . $kargs['namePrefix'] . $collectionName,
$collectionPath . '.:(format)',
array_merge($connectkargs, $options));
// Add the routes that deal with new resource methods
foreach ($newMethods as $method => $lst) {
$routeOptions = $this->_requirementsFor($method, $options);
foreach ($lst as $action) {
if ($action == 'new' && $newPath) {
$path = $newPath;
} else {
$path = sprintf('%s/%s', $newPath, $action);
}
$name = 'new_' . $memberName;
if ($action != 'new') {
$name = $action . '_' . $name;
}
$routeOptions['action'] = $action;
$this->connect($kargs['namePrefix'] . $name, $path, $routeOptions);
if ($action == 'new' && $newPath) {
$path = $newPath . '.:(format)';
} else {
$path = sprintf('%s/%s.:(format)', $newPath, $action);
}
$this->connect('formatted_' . $kargs['namePrefix'] . $name,
$path, $routeOptions);
}
}
$requirementsRegexp = '[\w\-_]+';
// Add the routes that deal with member methods of a resource
foreach ($memberMethods as $method => $lst) {
$routeOptions = $this->_requirementsFor($method, $options);
$routeOptions['requirements'] = array('id' => $requirementsRegexp);
if (!in_array($method, array('POST', 'GET', 'any'))) {
$primary = array_shift($lst);
} else {
$primary = null;
}
foreach ($lst as $action) {
$routeOptions['action'] = $action;
$this->connect(sprintf('%s%s_%s', $kargs['namePrefix'], $action, $memberName),
sprintf('%s/%s', $memberPath, $action),
$routeOptions);
$this->connect(sprintf('formatted_%s%s_%s', $kargs['namePrefix'], $action, $memberName),
sprintf('%s/%s.:(format)', $memberPath, $action),
$routeOptions);
}
if ($primary) {
$routeOptions['action'] = $primary;
$this->connect($memberPath, $routeOptions);
$this->connect($memberPath . '.:(format)', $routeOptions);
}
}
// Specifically add the member 'show' method
$routeOptions = $this->_requirementsFor('GET', $options);
$routeOptions['action'] = 'show';
$routeOptions['requirements'] = array('id' => $requirementsRegexp);
$this->connect($kargs['namePrefix'] . $memberName, $memberPath, $routeOptions);
$this->connect('formatted_' . $kargs['namePrefix'] . $memberName,
$memberPath . '.:(format)', $routeOptions);
}
/**
* Returns a new dict to be used for all route creation as
* the route options.
* @see resource()
*
* @param string $method Request method ('get', 'post', etc.) or 'any'
* @param array $options Assoc. array to populate with 'conditions' key
* @return $options populated
*/
protected function _requirementsFor($meth, $options)
{
if ($meth != 'any') {
$options['conditions'] = array('method' => array(Horde_String::upper($meth)));
}
return $options;
}
/**
* Swap the keys and values in the dict, and uppercase the values
* from the dict during the swap.
* @see resource()
*
* @param array $dct Input dict (assoc. array)
* @param array $newdct Output dict to populate
* @return array $newdct populated
*/
protected function _swap($dct, $newdct)
{
foreach ($dct as $key => $val) {
$newkey = Horde_String::upper($val);
if (!isset($newdct[$newkey])) {
$newdct[$newkey] = array();
}
$newdct[$newkey][] = $key;
}
return $newdct;
}
/**
* Sort an array of Horde_Routes_Routes to using _keycmp() for the comparision
* to order them ideally for matching.
*
* An unfortunate property of PHP's usort() is that if two members compare
* equal, their order in the sorted array is undefined (see PHP manual).
* This is unsuitable for us because the order that the routes were
* connected to the mapper is significant.
*
* Uses this method uses merge sort algorithm based on the
* comments in http://www.php.net/usort
*
* @param array $array Array Horde_Routes_Route objects to sort (by reference)
* @return void
*/
protected function _keysort(&$array)
{
// arrays of size < 2 require no action.
if (count($array) < 2) { return; }
// split the array in half
$halfway = count($array) / 2;
$array1 = array_slice($array, 0, $halfway);
$array2 = array_slice($array, $halfway);
// recurse to sort the two halves
$this->_keysort($array1);
$this->_keysort($array2);
// if all of $array1 is <= all of $array2, just append them.
if ($this->_keycmp(end($array1), $array2[0]) < 1) {
$array = array_merge($array1, $array2);
return;
}
// merge the two sorted arrays into a single sorted array
$array = array();
$ptr1 = 0;
$ptr2 = 0;
while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
if ($this->_keycmp($array1[$ptr1], $array2[$ptr2]) < 1) {
$array[] = $array1[$ptr1++];
}
else {
$array[] = $array2[$ptr2++];
}
}
// merge the remainder
while ($ptr1 < count($array1)) { $array[] = $array1[$ptr1++]; }
while ($ptr2 < count($array2)) { $array[] = $array2[$ptr2++]; }
return;
}
/**
* Compare two Horde_Route_Routes objects by their keys against
* the instance variable $keysortTmp. Used by _keysort().
*
* @param array $a First dict (assoc. array)
* @param array $b Second dict
* @return integer
*/
protected function _keycmp($a, $b)
{
$keys = $this->_keysortTmp;
$am = $a->minKeys;
$a = $a->maxKeys;
$b = $b->maxKeys;
$lendiffa = count(array_diff($keys, $a));
$lendiffb = count(array_diff($keys, $b));
// If they both match, don't switch them
if ($lendiffa == 0 && $lendiffb == 0) {
return 0;
}
// First, if $a matches exactly, use it
if ($lendiffa == 0) {
return -1;
}
// Or $b matches exactly, use it
if ($lendiffb == 0) {
return 1;
}
// Neither matches exactly, return the one with the most in common
if ($this->_cmp($lendiffa, $lendiffb) != 0) {
return $this->_cmp($lendiffa, $lendiffb);
}
// Neither matches exactly, but if they both have just as much in common
if (count($this->_arrayUnion($keys, $b)) == count($this->_arrayUnion($keys, $a))) {
return $this->_cmp(count($a), count($b));
// Otherwise, we return the one that has the most in common
} else {
return $this->_cmp(count($this->_arrayUnion($keys, $b)), count($this->_arrayUnion($keys, $a)));
}
}
/**
* Create a union of two arrays.
*
* @param array $a First array
* @param array $b Second array
* @return array Union of $a and $b
*/
protected function _arrayUnion($a, $b)
{
return array_merge(array_diff($a, $b), array_diff($b, $a), array_intersect($a, $b));
}
/**
* Equivalent of Python's cmp() function.
*
* @param integer|float $a First item to compare
* @param integer|flot $b Second item to compare
* @param integer Result of comparison
*/
protected function _cmp($a, $b)
{
if ($a < $b) {
return -1;
}
if ($a == $b) {
return 0;
}
return 1;
}
/**
* Trims slashes from the beginning or end of a part/URL.
*
* @param string $name Part or URL with slash at begin/end
* @return string Part or URL with begin/end slashes removed
*/
protected function _stripSlashes($name)
{
if (substr($name, 0, 1) == '/') {
$name = substr($name, 1);
}
if (substr($name, -1, 1) == '/') {
$name = substr($name, 0, -1);
}
return $name;
}
}