=== modified file '.htaccess' --- .htaccess 2009-01-09 02:49:01 +0000 +++ .htaccess 2009-02-18 01:50:03 +0000 @@ -80,7 +80,7 @@ DirectoryIndex index.php # # If your site is running in a VirtualDocumentRoot at http://example.com/, # uncomment the following line: - # RewriteBase / + RewriteBase /~lgarfiel/projects/drupal7-handlers # Rewrite URLs of the form 'x' to the form 'index.php?q=x'. RewriteCond %{REQUEST_FILENAME} !-f === modified file 'includes/bootstrap.inc' --- includes/bootstrap.inc 2009-01-31 16:50:56 +0000 +++ includes/bootstrap.inc 2009-02-18 03:30:30 +0000 @@ -1615,3 +1615,162 @@ function registry_rebuild() { /** * @} End of "ingroup registry". */ + +/** + * @ingroup handlers + * @{ + */ + +/** + * Request a handler object. + * + * @param $slot_id + * The internal ID of the slot for which we want to retrieve a handler. + * @param $target + * The target for which we want the attached handler. + * @return + * The associated handler object. + */ +function handler($slot_id, $target = 'default', $reset = FALSE) { + // $handler_mappings is an array, keyed by $slot_id and $target and the + // values are arrays containing class names and a boolean indicating + // reusability. $defaults contains class names and reusability information + // for slots that only have 'default' defined. handler_objects keeps the + // objects themselves. + static $handler_mappings, $handler_objects, $defaults; + + // We have to reset the target cache after a new handler has been attached + // or detached. + if ($reset) { + $handler_mappings = array(); + $handler_objects = array(); + $defaults = array(); + return; + } + + // If the only attached handler is attached to default, we cache that in + // the variable system to reduce database lookups. That includes any + // slot that does not make use of targets. Because the variable system + // doesn't auto-initialize in variable_get(), any handler lookup that runs + // before the variable system has been initialized will simply fail + // this check and use the database anyway. To save a few variable_get calls + // we static its results. + if (isset($defaults[$slot_id])) { + $record = $defaults[$slot_id]; + } + elseif ($record = variable_get('handler_default_' . $slot_id, array())) { + $record['default'] = TRUE; + $defaults[$slot_id] = $record; + } + // Look up the class for the requested slot/target. + elseif (empty($handler_mappings[$slot_id][$target])) { + // Try to get the associated handler. If the first query finds an associated + // handler, we use that. If not, the second query will always find the + // default handler. By UNIONing them together we get the fallback default + // behavior without having to issue a second request to the database. + $record = db_query_range("SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchAssoc(); + + // It's possible that this function will be called before the handler system + // has first been initialized. That's especially the case for early-running + // systems such as cache or path, as they operate during the bootstrap phase. + // If we have no record at all, we first rebuild the registry and then try + // again. That should at least always give us the slot-defined default + // and allow the system to proceed. + if (!$record) { + registry_rebuild(); + handlers_rebuild(); + $record = db_query_range("SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT class, reuse FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchAssoc(); + } + // Statically cache the lookup information so that we don't need to check + // for it again. + $handler_mappings[$slot_id][$target] = $record; + } + else { + $record = $handler_mappings[$slot_id][$target]; + } + + // If the handler is reusable when only the default target is used, then + // the same object can be used for all targets. + if (!empty($record['default']) && $record['reuse']) { + $target = 'default'; + } + + // If we already have the appropriate handler cached, just use that. Otherwise + // create a new handler object. + if (!empty($handler_objects[$slot_id][$target])) { + $handler = $handler_objects[$slot_id][$target]; + } + else { + $handler = new $record['class']($target); + + // Cache the handler object for later use, if flagged to do so. + if ($record['reuse']) { + $handler_objects[$slot_id][$target] = $handler; + } + } + + return $handler; +} + +/** + * Rebuild the handler and slot registries. + */ +function handlers_rebuild() { + require_once DRUPAL_ROOT . '/includes/handlers.inc'; + handler_slot_build(); + handler_handler_build(); + handler_ensure_defaults(); +} + +/** + * Interface for all Handler classes for any slot. + * + * This is a very thin interface, but does serve to help standardize handler + * behavior and a way to type-check a handler object. Interfaces for specific + * slots should extend this interface. + */ +interface HandlerInterface { + + /** + * Constructor + * + * @param $target + * The target for which this handler is being called. Some handlers may + * require this information in order to route commands properly. + */ + function __construct($target = 'default'); +} + +/** + * Base implementation of a handler object. + * + * Simple handler objects may choose to inherit from a base class in order to + * not reimplement routine functionality. Alternatively they may simply implement + * the appropriate interface and implement their own version of common + * functionality. Both methods are acceptable depending on the use case. + */ +abstract class HandlerBase implements HandlerInterface { + + /** + * The target for which this handler is active. + */ + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } +} + +/** + * @} End of "ingroup handlers". + */ === modified file 'includes/common.inc' --- includes/common.inc 2009-02-13 04:43:00 +0000 +++ includes/common.inc 2009-02-18 01:50:03 +0000 @@ -151,7 +151,7 @@ function drupal_get_html_head() { * Reset the static variable which holds the aliases mapped for this request. */ function drupal_clear_path_cache() { - drupal_lookup_path('wipe'); + handler('path_lookup')->clearCache(); } /** @@ -3254,7 +3254,7 @@ function drupal_render_page($page) { * * Recursively iterates over each of the array elements, generating HTML code. * - * HTML generation is controlled by two properties containing theme functions, + * HTML generation is controlled by two properties containing theme functions, * #theme and #theme_wrapper. * * #theme is the theme function called first. If it is set and the element has any @@ -3265,13 +3265,13 @@ function drupal_render_page($page) { * * The theme function in #theme_wrapper will be called after #theme has run. It * can be used to add further markup around the rendered children, e.g. fieldsets - * add the required markup for a fieldset around their rendered child elements. + * add the required markup for a fieldset around their rendered child elements. * A wrapper theme function always has to include the element's #children property - * in its output, as this contains the rendered children. + * in its output, as this contains the rendered children. * * For example, for the form element type, by default only the #theme_wrapper * property is set, which adds the form markup around the rendered child elements - * of the form. This allows you to set the #theme property on a specific form to + * of the form. This allows you to set the #theme property on a specific form to * a custom theme function, giving you complete control over the placement of the * form's children while not at all having to deal with the form markup itself. * @@ -3305,7 +3305,7 @@ function drupal_render(&$elements) { else { $elements += element_basic_defaults(); } - + // If #markup is not empty and no theme function is set, use theme_markup. // This allows to specify just #markup on an element without setting the #type. if (!empty($elements['#markup']) && empty($elements['#theme'])) { @@ -4186,6 +4186,7 @@ function drupal_flush_all_caches() { _drupal_flush_css_js(); registry_rebuild(); + handlers_rebuild(); drupal_clear_css_cache(); drupal_clear_js_cache(); @@ -4228,3 +4229,129 @@ function _drupal_flush_css_js() { } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * @ingroup handlers + * @{ + */ + +/** + * Associate a given handler to a give slot for the specified target. + * + * @param $slot_id + * The internal ID of the slot. + * @param $handler_id + * The internal ID of the hand + * @param $target + * The target for which to associate this handler to this slot. + */ +function handler_attach($slot_id, $handler_id, $target = 'default') { + + // Lazy-load the utility function. + drupal_function_exists('handler_load'); + + $handler = handler_load($slot_id, $handler_id); + $slot = slot_load($slot_id); + + if ($handler) { + // We store the class in the database in addition to the handler ID so that + // we can, on lookup, get back the class to instantiate and immediately + // do so. + db_merge('handler_attachments') + ->key(array( + 'slot' => $slot_id, + 'target' => $target, + )) + ->fields(array( + 'handler' => $handler->handler, + 'class' => $handler->class, + 'reuse' => (int)$slot->reuse, + )) + ->execute(); + } + + // If we've associated something other than the default target, disable the + // extra variable system caching of handler attachments. + if ($target != 'default') { + variable_del('handler_default_' . $slot_id); + } + + // Flush the target cache. + handler(NULL, NULL, TRUE); +} + +/** + * Revert a given slot and target to the default handler. + * + * @param $slot_id + * The slot to reset to the default handler. + * @param $target + * The target to reset to the default handler. + */ +function handler_detach($slot_id, $target = 'default') { + + // Delete the handler attachment. + db_delete('handler_attachments') + ->condition('slot', $slot_id) + ->condition('target', $target) + ->execute(); + + // If we just deleted the default target for this slot, reattach the + // slot-defined default handler. That ensures that we always have at least + // one handler configured. + if ($target = 'default') { + if (drupal_function_exists('slot_load')) { + $slot = slot_load($slot_id); + handler_attach($slot_id, $slot->default_handler, 'default'); + } + } + + // If there's no attachments for this slot other than the default, cache + // that to the variable system for faster lookups. + $attachments = db_query("SELECT target, class, reuse FROM {handler_attachments} WHERE slot = :slot", array(':slot' => $slot_id))->fetchAllAssoc('target', PDO::FETCH_ASSOC); + if (count($attachments) == 1 && key($attachments) == 'default') { + $attachments = current($attachments); + unset($attachments['target']); + variable_set('handler_default_' . $slot_id, $attachments); + } + + // Flush the target cache. + handler(NULL, NULL, TRUE); +} + +/** + * Retrieve a list of all available handlers for a given slot. + * + * @param $slot_id + * The slot for which we want a list of available handlers. + * @return + * An array of handlers that have been defined for this slot. + */ +function handler_get_available_handlers($slot_id) { + return db_query('SELECT handler, title, description, class FROM {handler_info} WHERE slot = :slot ORDER BY title', array(':slot' => $slot_id))->fetchAllAssoc('handler'); +} + +/** + * Get the handler object for the handler associated to this slot/target. + * + * @param $slot_id + * The slot for which we want to look up a handler. + * @param $target + * The target for which we want to look up a handler. + * @return + * A loaded handler object. + */ +function handler_get_attached_handler($slot_id, $target = 'default') { + $handler_id = db_query_range("SELECT handler FROM {handler_attachments} WHERE slot = :slot_1 AND target = :target + UNION SELECT handler FROM {handler_attachments} WHERE slot = :slot_2 AND target = 'default'", array( + ':slot_1' => $slot_id, + ':slot_2' => $slot_id, + ':target' => $target, + ), 0, 1)->fetchField(); + + return handler_load($slot_id, $handler_id); +} + +/** + * @} End of "ingroup handlers". + */ === modified file 'includes/path.inc' --- includes/path.inc 2009-01-04 20:04:32 +0000 +++ includes/path.inc 2009-02-18 01:50:03 +0000 @@ -23,84 +23,6 @@ function drupal_init_path() { } /** - * Given an alias, return its Drupal system URL if one exists. Given a Drupal - * system URL return one of its aliases if such a one exists. Otherwise, - * return FALSE. - * - * @param $action - * One of the following values: - * - wipe: delete the alias cache. - * - alias: return an alias for a given Drupal system path (if one exists). - * - source: return the Drupal system URL for a path alias (if one exists). - * @param $path - * The path to investigate for corresponding aliases or system URLs. - * @param $path_language - * Optional language code to search the path with. Defaults to the page language. - * If there's no path defined for that language it will search paths without - * language. - * - * @return - * Either a Drupal system path, an aliased path, or FALSE if no path was - * found. - */ -function drupal_lookup_path($action, $path = '', $path_language = '') { - global $language; - // $map is an array with language keys, holding arrays of Drupal paths to alias relations - static $map = array(), $no_src = array(), $count; - - $path_language = $path_language ? $path_language : $language->language; - - // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases - if (!isset($count)) { - $count = db_query('SELECT COUNT(pid) FROM {url_alias}')->fetchField(); - } - - if ($action == 'wipe') { - $map = array(); - $no_src = array(); - $count = NULL; - } - elseif ($count > 0 && $path != '') { - if ($action == 'alias') { - if (isset($map[$path_language][$path])) { - return $map[$path_language][$path]; - } - // Get the most fitting result falling back with alias without language - $alias = db_query("SELECT dst FROM {url_alias} WHERE src = :src AND language IN(:language, '') ORDER BY language DESC", array( - ':src' => $path, - ':language' => $path_language)) - ->fetchField(); - $map[$path_language][$path] = $alias; - return $alias; - } - // Check $no_src for this $path in case we've already determined that there - // isn't a path that has this alias - elseif ($action == 'source' && !isset($no_src[$path_language][$path])) { - // Look for the value $path within the cached $map - $src = ''; - if (!isset($map[$path_language]) || !($src = array_search($path, $map[$path_language]))) { - // Get the most fitting result falling back with alias without language - if ($src = db_query("SELECT src FROM {url_alias} WHERE dst = :dst AND language IN(:language, '') ORDER BY language DESC", array( - ':dst' => $path, - ':language' => $path_language)) - ->fetchField()) { - $map[$path_language][$src] = $path; - } - else { - // We can't record anything into $map because we do not have a valid - // index and there is no need because we have not learned anything - // about any Drupal path. Thus cache to $no_src. - $no_src[$path_language][$path] = TRUE; - } - } - return $src; - } - } - - return FALSE; -} - -/** * Given an internal Drupal path, return the alias set by the administrator. * * @param $path @@ -113,11 +35,18 @@ function drupal_lookup_path($action, $pa * found. */ function drupal_get_path_alias($path, $path_language = '') { - $result = $path; - if ($alias = drupal_lookup_path('alias', $path, $path_language)) { - $result = $alias; + static $handler; + + // This function may be called many times in one page, so static cache the + // handler so that we don't waste cycles on the handler lookup each time. + // The handler should never change during the course of a page request so + // that doesn't cause a problem. Note that because objects pass by handle + // this will be the same handler objec as in drupal_get_normal_path(). + if (empty($handler)) { + $handler = handler('path_lookup'); } - return $result; + + return $handler->lookupAlias($path, $path_language); } /** @@ -127,16 +56,26 @@ function drupal_get_path_alias($path, $p * A Drupal path alias. * @param $path_language * An optional language code to look up the path in. - * * @return * The internal path represented by the alias, or the original alias if no * internal path was found. */ function drupal_get_normal_path($path, $path_language = '') { - $result = $path; - if ($src = drupal_lookup_path('source', $path, $path_language)) { - $result = $src; + static $handler; + + // This function may be called many times in one page, so static cache the + // handler so that we don't waste cycles on the handler lookup each time. + // The handler should never change during the course of a page request so + // that doesn't cause a problem. Note that because objects pass by handle + // this will be the same handler objec as in drupal_get_path_alias(). + if (empty($handler)) { + $handler = handler('path_lookup'); } + + $result = $handler->lookupPath($path, $path_language); + + // @todo: Remove this once we confirm that it's not needed, as alternate + // handler implementations can probably handle it. if (function_exists('custom_url_rewrite_inbound')) { // Modules may alter the inbound request path by reference. custom_url_rewrite_inbound($result, $path, $path_language); === modified file 'includes/registry.inc' --- includes/registry.inc 2008-12-20 18:24:32 +0000 +++ includes/registry.inc 2009-02-18 01:50:03 +0000 @@ -37,6 +37,11 @@ function _registry_rebuild() { require_once DRUPAL_ROOT . '/includes/database/select.inc'; require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc'; + // If called before bootstrap completes, these files may not be available yet. + require_once DRUPAL_ROOT . '/includes/common.inc'; + require_once DRUPAL_ROOT . '/includes/file.inc'; + require_once DRUPAL_ROOT . '/modules/system/system.module'; + // Reset the resources cache. _registry_get_resource_name(); // Get the list of files we are going to parse. === modified file 'modules/path/path.test' --- modules/path/path.test 2009-01-09 07:44:00 +0000 +++ modules/path/path.test 2009-02-18 01:50:03 +0000 @@ -186,7 +186,7 @@ class PathLanguageTestCase extends Drupa $this->drupalPost(NULL, $edit, t('Save')); // Clear the path lookup cache. - drupal_lookup_path('wipe'); + drupal_clear_path_cache(); // Ensure the node was created. // Check to make sure the node was created. === modified file 'modules/system/system.admin.inc' --- modules/system/system.admin.inc 2009-02-11 05:33:18 +0000 +++ modules/system/system.admin.inc 2009-02-18 01:50:03 +0000 @@ -1417,6 +1417,46 @@ function system_clear_cache_submit(&$for drupal_set_message(t('Caches cleared.')); } + +/** + * Form builder; Configure the path alias lookup engine. + * + * @ingroup forms + */ +function system_path_lookup_form() { + $form = array(); + + $options = array(); + foreach (handler_get_available_handlers('path_lookup') as $handler => $info) { + $options[$handler] = t('@title (%description)', array( + '@title' => t($info->title), + '%description' => t($info->description), + )); + } + + $handler = handler_get_attached_handler('path_lookup'); + + $form['path_lookup'] = array( + '#title' => 'Path lookup mechanism', + '#type' => 'radios', + '#options' => $options, + '#default_value' => $handler->handler, + ); + + $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') ); + + return $form; +} + +/** + * Submit callback; save the path lookup handler selection. + * + * @ingroup forms + */ +function system_path_lookup_form_submit($form, &$form_state) { + handler_attach('path_lookup', $form_state['values']['path_lookup']); +} + /** * Form builder; Configure the site file handling. * === modified file 'modules/system/system.api.php' --- modules/system/system.api.php 2009-02-09 15:42:52 +0000 +++ modules/system/system.api.php 2009-02-18 01:50:03 +0000 @@ -1635,6 +1635,144 @@ function hook_disable() { mymodule_cache_rebuild(); } + +/** + * Define one or more slots for the handlers system. + * + * This hook should return a nested array of slot definitions. Each key + * in the array is the machine-readable slot name, and its value is an array + * of values that define the slot. + * + * title + * This is the human-readable name of the slot. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * description + * A human-readable description of what the slot is for. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * interface + * The PHP interface that defines this slot. The interface must extend + * HandlerInterface, but otherwise may define whatever methods it wants. + * It may also be placed in any file, depending on what would provide the + * most performance for autoloading. + * default_handler (optional) + * The machine-readable name of the default handler for this slot, as defined + * in hook_handler_info(). All slots must have at least one handler + * available. If not specified, "default" is used. + * reuse (optional) + * By default, if a given target on a slot is requested multiple times in + * the same page request the same object will be returned each time. The + * object will be statically cached. To disable that behavior and return + * a newly instantiated object for every request, set this value to FALSE. + * + * This is a registry-style function. It should normally be placed in the + * file $module.registry.inc. + * + * @return + * A slot definition array. + */ +function hook_slot_info() { + return array( + 'fancystring' => array( + 'title' => 'Fancy string', + 'description' => 'Do fancy stuff to strings', + 'interface' => 'FancystringInterface', + 'default_handler' => 'default', + 'reuse' => FALSE, + ), + 'thingie' => array( + 'title' => 'Other slot', + 'description' => 'This slot does nothing. It\'s just to test lookup behavior of reusable slots.', + 'interface' => 'ThingieInterface', + ), + ); +} + +/** + * Alter the slot definitions of another module. + * + * @param $info + * The complete slot definition array from hook_slot_info(). + */ +function hook_slot_info_alter(&$info) { + // Change the default handler. + $info['fancystring']['default_handler'] = 'string_mangler'; +} + +/** + * Define one or more handlers in the system. + * + * This hook should return a doubly-nested array of handler definitions. The + * first key is an existing slot. Its value is an associative array of handler + * machine-readable names and handler definitions. + * + * Each handler definition is an associative array of values that define the + * handler. + * + * title + * This is the human-readable name of the handler. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * description + * A human-readable description of the handler. Note that this value + * should not be run through t() as it will be stored in the database. It + * should be translated when displayed to the user. + * class + * The PHP class that defines this handler. The class must implement the + * interface defined for this slot, but otherwise may be defined in any way + * desired, including implementing the interface directly or indirectly by + * subclassing another class that implements that interface. It may also be + * placed in any file, depending on what would provide the most performance + * for autoloading. + * + * This is a registry-style function. It should normally be placed in the + * file $module.registry.inc. + * + * @return + * A handler definition array. + */ +function hook_handler_info() { + return array( + 'fancystring' => array( + 'default' => array( + // Translate on load, not define, like hook_menu(). + 'title' => 'No string mutation', + 'class' => 'FancystringDefault', + 'description' => "This handler doesn't do anything to the string. It passes through unaltered.", + ), + 'rot13' => array( + 'title' => 'Rot13 translation', + 'class' => 'FancystringRot13', + 'description' => 'This handler ROT13 encrypts a string.', + ), + 'custom_translate' => array( + 'title' => 'Custom mapping', + 'class' => 'FancystringCustom', + 'description' => 'This handler uses a user-specified mapping array.', + ), + ), + 'thingie' => array( + 'default' => array( + 'title' => 'Do nothing handler', + 'description' => 'This handler does nothing', + 'class' => 'ThingieDefault', + ) + ), + ); +} + +/** + * Alter the handler definitions of another module. + * + * @param $info + * The complete handler definition array from hook_handler_info(). + */ +function hook_handler_info_alter(&$info) { + // change the class used by the rot13 handler. + $info['fancystring']['rot13']['class'] = 'FancystringRot13Alternate'; +} + /** * @} End of "addtogroup hooks". */ === modified file 'modules/system/system.info' --- modules/system/system.info 2008-10-12 01:23:01 +0000 +++ modules/system/system.info 2009-02-18 01:50:03 +0000 @@ -8,4 +8,6 @@ files[] = system.module files[] = system.admin.inc files[] = image.gd.inc files[] = system.install +files[] = system.path_lazy.inc +files[] = system.path_precache.inc required = TRUE === modified file 'modules/system/system.install' --- modules/system/system.install 2009-02-03 12:30:14 +0000 +++ modules/system/system.install 2009-02-18 01:50:03 +0000 @@ -726,6 +726,122 @@ function system_schema() { 'nid' => array('nid'), ), ); + + $schema['handler_attachments'] = array( + 'description' => 'Maps slots and targets to their configured handler.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'target' => array( + 'description' => 'The target value.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'handler' => array( + 'description' => 'The handler attached to this slot/target.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target. It is stored in this table to make lookups faster.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not this handler may be reused. It is stored in this table to make lookups faster.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('slot', 'target'), + ); + + $schema['handler_info'] = array( + 'description' => 'Tracks all available handlers in the system.', + 'fields' => array( + 'handler' => array( + 'description' => 'The machine-readable name of the handler.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'slot' => array( + 'description' => 'The machine-readable name of the slot this handler is for.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class that defines this handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this handler.', + 'type' => 'text', + 'not null' => TRUE, + ) + ), + 'primary key' => array('handler', 'slot'), + ); + + $schema['handler_slot_info'] = array( + 'description' => 'Tracks all registered slots in the system that handlers can fulfill.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'interface' => array( + 'description' => 'The PHP interface that defines this slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not to reuse handler objects on this slot when requested multiple times.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'default_handler' => array( + 'description' => 'The default handler id for this slot if one is not otherwise configured.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this slot.', + 'type' => 'text', + 'not null' => FALSE, + ), + ), + 'primary key' => array('slot'), + ); + $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array( @@ -3213,6 +3329,134 @@ function system_update_7018() { } /** + * Add the new tables to support handlers. + */ +function system_update_7019() { + $ret = array(); + + $schema['handler_attachments'] = array( + 'description' => 'Maps slots and targets to their configured handler.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'target' => array( + 'description' => 'The target value.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'handler' => array( + 'description' => 'The handler attached to this slot/target.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target. It is stored in this table to make lookups faster.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not this handler may be reused. It is stored in this table to make lookups faster.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('slot', 'target'), + ); + + $schema['handler_info'] = array( + 'description' => 'Tracks all available handlers in the system.', + 'fields' => array( + 'handler' => array( + 'description' => 'The machine-readable name of the handler.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'slot' => array( + 'description' => 'The machine-readable name of the slot this handler is for.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'class' => array( + 'description' => 'The PHP class that defines this handler.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this handler.', + 'type' => 'text', + 'not null' => TRUE, + ) + ), + 'primary key' => array('handler', 'slot'), + ); + + $schema['handler_slot_info'] = array( + 'description' => 'Tracks all registered slots in the system that handlers can fulfill.', + 'fields' => array( + 'slot' => array( + 'description' => 'The machine-readable name of the slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'title' => array( + 'description' => 'The human-readable name of the slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + 'interface' => array( + 'description' => 'The PHP interface that defines this slot.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'reuse' => array( + 'description' => 'Whether or not to reuse handler objects on this slot when requested multiple times.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'default_handler' => array( + 'description' => 'The default handler id for this slot if one is not otherwise configured.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A human-readable description of this slot.', + 'type' => 'text', + 'not null' => FALSE, + ), + ), + 'primary key' => array('slot'), + ); + + foreach ($schema as $table => $info) { + db_create_table($ret, $table, $info); + } + + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ === modified file 'modules/system/system.module' --- modules/system/system.module 2009-02-11 05:33:18 +0000 +++ modules/system/system.module 2009-02-18 01:50:03 +0000 @@ -711,6 +711,12 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['admin/settings/path-lookup'] = array( + 'title' => 'Path lookup', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('system_path_lookup_form'), + 'access arguments' => array('administer site configuration'), + ); // Reports: $items['admin/reports'] = array( @@ -2290,3 +2296,102 @@ function theme_meta_generator_header($ve function system_image_toolkits() { return array('gd'); } + +/** + * Implementation of hook_slot_info(). + */ +function system_slot_info() { + $slots['path_lookup'] = array( + 'title' => 'Path aliases', + 'description' => 'Lookup engine for path aliases', + 'interface' => 'PathAliasInterface', + 'default_handler' => 'lazy', + ); + + return $slots; +} + +/** + * Implementation of hook_handler_info(). + */ +function system_handler_info() { + $handlers['path_lookup']['lazy'] = array( + 'title' => 'Lazy lookup', + 'description' => 'Aliases are looked up one at a time as they are requested. Good for sites with a very large number of path aliases.', + 'class' => 'PathAliasLazy', + ); + $handlers['path_lookup']['precache'] = array( + 'title' => 'Pre-caching', + 'description' => 'All aliases are loaded from the database at once and then looked up in memory. Good for sites with a much smaller number of path aliases than pages.', + 'class' => 'PathAliasPrecache', + ); + + return $handlers; +} + +/** + * Interface for path alias lookup handlers. + */ +interface PathAliasInterface extends HandlerInterface { + + /** + * Given a system path, get the appropriate alias if any. + * + * @param $path + * The system path for which we want the appropriate alias. + * @param path_language + * Optional language code to search the path with. Defaults to the page language. + * If there's no path defined for that language it will search paths without + * language. + * @return + * An aliased path if one was found, or the original path if no alias was + * found. + */ + public function lookupAlias($path = '', $path_language = ''); + + /** + * Given an aliased path, get the corresponding system path, if any. + * + * @param $dest + * The alias for which we want the Drupal system path. + * @param path_language + * Optional language code to search the path with. Defaults to the page language. + * If there's no path defined for that language it will search paths without + * language. + * @return + * The internal path represented by the alias, or the original alias if no + * internal path was found. + */ + function lookupPath($dest = '', $path_language = ''); + + /** + * Clear the cache of looked up aliases so far this request. + */ + public function clearCache(); + +} + +abstract class PathAliasBase extends HandlerBase implements PathAliasInterface { + + /** + * An array with language keys, holding arrays of Drupal paths to alias relations. + * + * @var array + */ + protected $map; + + /** + * Keeps track of the number of paths in the system. + * + * If this value is 0, we know to not bother looking up any paths. + * + * @var int + */ + protected $count; + + public function clearCache() { + $this->map = array(); + $this->noSrc = array(); + $this->count = NULL; + } +}