Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.269 diff -u -p -r1.269 bootstrap.inc --- includes/bootstrap.inc 31 Jan 2009 16:50:56 -0000 1.269 +++ includes/bootstrap.inc 6 Feb 2009 14:55:25 -0000 @@ -1615,3 +1615,79 @@ 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') { + + // @TODO: Refactor to put the slot cache flag into the attachment table. + // Also force a record for the default target at all times, including in + // detach(). That way we can do a union query for the requested target + // and default target, in that order, and always get back the record + // we want, which is just cool. :-) + + static $handlers; + + if (empty($handlers[$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)->fetchObject(); + + // Create the handler. + $handler = new $record->class($target); + + // Cache the handler for later requests only if this slot is defined to do so. + if ($record->reuse) { + $handlers[$slot_id][$target] = $handler; + } + } + else { + $handler = $handlers[$slot_id][$target]; + } + + return $handler; +} + +/** + * Interface for all Handler classes for any slot. + * + * This is a very thin interface, but does serve to help standardize handler + * behavior, especially interaction with an environment 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'); +} + +/** + * @} End of "ingroup handlers". + */ Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.864 diff -u -p -r1.864 common.inc --- includes/common.inc 5 Feb 2009 01:21:16 -0000 1.864 +++ includes/common.inc 6 Feb 2009 14:55:25 -0000 @@ -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'])) { @@ -3479,7 +3479,7 @@ function element_child($key) { function element_children($element) { $keys = array(); foreach(array_keys($element) as $key) { - if ($key[0] !== '#') { + if ($key[0] !== '#') { $keys[] = $key; } } @@ -4161,6 +4161,7 @@ function drupal_flush_all_caches() { _drupal_flush_css_js(); registry_rebuild(); + handlers_rebuild(); drupal_clear_css_cache(); drupal_clear_js_cache(); @@ -4203,3 +4204,85 @@ function _drupal_flush_css_js() { } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * @ingroup handlers + * @{ + */ + +/** + * 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(); +} + +/** + * 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 rather than 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( + 'class' => $handler->class, + 'reuse' => (int)$slot->reuse, + )) + ->execute(); + } +} + +/** + * 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'); + } + } +} + + +/** + * @} End of "ingroup handlers". + */ Index: includes/handlers.inc =================================================================== RCS file: includes/handlers.inc diff -N includes/handlers.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/handlers.inc 6 Feb 2009 14:55:25 -0000 @@ -0,0 +1,272 @@ +someMethod(); + * @endcode + * + * If no handler has been attached to that slot and target, the handler attached + * to the target "default" is used instead. A slot/target may also be "reset" + * to the default handler: + * + * @code + * handler_detach('slot_name', 'target'); + * @endcode + * + * If no target is specified, "default" is assumed. That allows for a given + * slot to not make use of the system's multi-target capabilities by simply + * omitting the target whenever it is called, which results in a target of + * "default" being used in all cases. + */ + +/** + * Rebuild the handler slot info registry. + * + * @return + * The array of slots just declared. + */ +function handler_slot_build() { + + $slots = module_invoke_all('slot_info'); + + // Set default values, to avoid NULL issues if nothing else. + foreach ($slots as $slot => $info) { + $slots[$slot] += handler_slot_defaults(); + } + + // Let other modules alter the handler target registry if needed. + drupal_alter('slot_info', $slots); + + // Build the target data. + $insert = db_insert('handler_slot_info')->fields(array('slot', 'title', 'interface', 'reuse', 'default_handler', 'description')); + + foreach ($slots as $slot => $info) { + $insert->values(array( + 'slot' => $slot, + 'title' => $info['title'], + 'interface' => $info['interface'], + 'reuse' => (int)$info['reuse'], + 'default_handler' => $info['default_handler'], + 'description' => $info['description'], + )); + } + + // Don't do the deletion until after we've gotten the new data prepared. That + // reduces the potential race condition window. + db_delete('handler_slot_info')->execute(); + $insert->execute(); + + return $slots; +} + +/** + * Rebuild the handler info registry. + * + * @return + * The array of handlers just defined. + */ +function handler_handler_build() { + + $handlers = module_invoke_all('handler_info'); + + // Set default values, to avoid NULL issues if nothing else. + foreach ($handlers as $slot => $handler_info) { + foreach ($handler_info as $handler => $info) { + $handlers[$slot][$handler] += handler_handler_defaults(); + } + } + + // Let other modules alter the handler registry if needed. + drupal_alter('handler_info', $handlers); + + // Build the handler data. + $insert = db_insert('handler_info')->fields(array('handler', 'slot', 'title', 'class', 'description')); + foreach ($handlers as $slot => $handler_info) { + foreach ($handler_info as $handler => $info) { + $insert->values(array( + 'handler' => $handler, + 'slot' => $slot, + 'title' => $info['title'], + 'class' => $info['class'], + 'description' => $info['description'], + )); + } + } + + // Don't do the deletion until after we've gotten the new data. That reduces + // the potential race condition window. + db_delete('handler_info')->execute(); + + $insert->execute(); + + return $handlers; +} + +/** + * Ensure that every slot has a handler attached for the default target. + * + * For every slot, attach whatever handler is declared the default handler + * to the default target unless one has already been defined. That ensures + * that the attachment table always has at least that default record in it for + * each slot, which greatly simplifies the lookup logic. + */ +function handler_ensure_defaults() { + $result = db_query("SELECT hsi.slot, reuse, class + FROM {handler_slot_info} hsi + INNER JOIN {handler_info} hi ON hsi.slot = hi.slot AND hsi.default_handler = hi.handler"); + + foreach ($result as $record) { + // We can't exclude both fields or the query breaks. However, setting + // reuse to itself has no effect on the table so it's safe to do. + db_merge('handler_attachments') + ->key(array( + 'slot' => $record->slot, + 'target' => 'default', + )) + ->fields(array( + 'class' => $record->class, + 'reuse' => $record->reuse, + )) + ->updateExcept('class') + ->execute(); + } +} + +/** + * Define various default values for handler slots. + * + * @return + * The default "empty" slot definition. + */ +function handler_slot_defaults() { + return array( + 'slot' => '', + 'title' => '', + 'interface' => 'HandlerInterface', + 'reuse' => 1, + 'default_handler' => 'default', + 'description' => '', + ); +} + +/** + * Define various default values for handlers. + * + * @return + * The default "empty" handler definition. + */ +function handler_handler_defaults() { + return array( + 'handler' => '', + 'slot' => '', + 'title' => '', + 'class' => '', + 'description' => '', + ); +} + +/** + * Load function for slot info objects. + * + * @param $slot_id + * The internal slot ID. + * @param $refresh + * If this is set to TRUE, the internal slot cache will be flushed. + * @return + * The slot object or NULL if it is not defined. + */ +function slot_load($slot_id, $refresh = FALSE) { + static $slots; + + if ($refresh) { + $slots = array(); + } + + if (empty($slots[$slot_id])) { + $slots[$slot_id] = db_query("SELECT slot, title, interface, reuse, default_handler, description FROM {handler_slot_info} WHERE slot = :slot_id", array(':slot_id' => $slot_id))->fetchObject(); + } + + return $slots[$slot_id]; +} + +/** + * Load function for handler info objects. + * + * @param $slot_id + * The internal slot ID. All handlers are namespaced within their slot. + * @param $handler_id + * The internal handler ID. + * @param $refresh + * If this is set to TRUE, the internal handler cache will be flushed. + * @return + * The handler object or NULL if not defined. + */ +function handler_load($slot_id, $handler_id, $refresh = FALSE) { + static $handlers; + + if ($refresh) { + $handlers = array(); + } + + if (empty($handlers[$slot_id][$handler_id])) { + $handlers[$slot_id][$handler_id] = db_query("SELECT handler, slot, title, class, description FROM {handler_info} WHERE slot = :slot_id AND handler = :handler_id", array( + ':slot_id' => $slot_id, + ':handler_id' => $handler_id, + ))->fetchObject(); + } + + return $handlers[$slot_id][$handler_id]; +} + +/** + * @} End of "ingroup handlers". + */ Index: modules/simpletest/tests/handlers.test =================================================================== RCS file: modules/simpletest/tests/handlers.test diff -N modules/simpletest/tests/handlers.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers.test 6 Feb 2009 14:55:25 -0000 @@ -0,0 +1,204 @@ + t('Handlers, Nonreusable'), + 'description' => t('Test the Handler routing system'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp('handlers_test'); + + // We need to rebuild the index at least once, because the module_enable() + // routine called by the parent method doesn't call drupal_flush_all_caches(). + handlers_rebuild(); + } + + /** + * Test that we can retrieve the default handler if no target is defined. + */ + function testDefaultHandler() { + // 'default' has not been attached to anything, so should return the + // default implementation. + $handler = handler('fancystring', 'default'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Default handler returned correct string.')); + } + } + + /** + * Test that we can specify a handler for a target and it loads correctly. + */ + function testSpecifiedHandler() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now request the handler. + $handler = handler('fancystring', 'foo'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringRot13, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual(str_rot13($this->sampleString), $mangled, t('Specified handler returned correct string.')); + } + } + + /** + * Test that we can skip using target on a slot and everything still works. + */ + function testUndefinedHandler() { + // An undefined target should always give us the default handler defined by the slot. + $handler = handler('fancystring'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Default handler returned correct string.')); + } + } + + /** + * Test a context-sensitive handler. + */ + function testContextSensitiveHandler() { + + // This is a poor way of mocking up the variable system, but it will have + // to do for now. + $map = array( + 'Hello' => 'Goodbye', + 'World' => 'Cruel World', + ); + $GLOBALS['conf']['fancystring_translate'] = $map; + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'custom_translate', 'bar'); + + // Now request the handler. + $handler = handler('fancystring', 'bar'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringCustom, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual(strtr($this->sampleString, $map), $mangled, t('Specified handler returned correct string.')); + } + + unset ($GLOBALS['conf']['fancystring_translate']); + } + + /** + * Test that we can successfully unset a handler attachment. + */ + function testUnsettingHandler() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now unset that attachment. + handler_detach('fancystring', 'foo'); + + // Now request the handler. We should get back the default. + $handler = handler('fancystring', 'foo'); + + $this->assertTrue($handler instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler instanceof FancystringDefault, t('Correct handler object returned.')); + + if ($handler instanceof HandlerInterface) { + $handler->setString($this->sampleString); + $mangled = $handler->getString(); + + $this->assertEqual($this->sampleString, $mangled, t('Unspecified handler returned correct string.')); + } + } + + /** + * Test that a non-reusable slot returns a new object eacn time. + */ + function testHandlerReuse() { + + // Specify a specific handler for a given target. + handler_attach('fancystring', 'rot13', 'foo'); + + // Now request the handler. + $handler_1 = handler('fancystring', 'foo'); + + $this->assertTrue($handler_1 instanceof FancystringInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler_1 instanceof FancystringRot13, t('Correct handler object returned.')); + + $handler_2 = handler('fancystring', 'foo'); + + $this->assertNotIdentical($handler_1, $handler_2, t('A new object was returned.')); + } +} + +/** + * Test class for the Handlers system, part 2. + */ +class HandlersReusableTestCase extends DrupalWebTestCase { + + protected $sampleString = 'Hello World'; + + function getInfo() { + return array( + 'name' => t('Handlers, Reusable'), + 'description' => t('Test the Handler routing system for reusable handlers.'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp('handlers_test'); + + // We need to rebuild the index at least once, because the module_enable() + // routine called by the parent method doesn't call drupal_flush_all_caches(). + handlers_rebuild(); + } + + /** + * Test that a reusable slot returns the exact same object each time. + */ + function testHandlerReuse() { + + // Specify a specific handler for a given target. + handler_attach('thingie', 'default', 'foo'); + + // Now request the handler. + $handler_1 = handler('thingie', 'foo'); + + $this->assertTrue($handler_1 instanceof ThingieInterface, t('Returned handler has the correct interface.')); + $this->assertTrue($handler_1 instanceof ThingieDefault, t('Correct handler object returned.')); + + $handler_2 = handler('thingie', 'foo'); + + $this->assertIdentical($handler_1, $handler_2, t('The same handler object was returned.')); + } +} Index: modules/simpletest/tests/handlers_test.info =================================================================== RCS file: modules/simpletest/tests/handlers_test.info diff -N modules/simpletest/tests/handlers_test.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers_test.info 6 Feb 2009 14:55:25 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = "Handlers Test" +description = "Support module for Handlers tests." +core = 7.x +package = Testing +files[] = handlers_test.module +version = VERSION +hidden = TRUE Index: modules/simpletest/tests/handlers_test.module =================================================================== RCS file: modules/simpletest/tests/handlers_test.module diff -N modules/simpletest/tests/handlers_test.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/handlers_test.module 6 Feb 2009 14:55:25 -0000 @@ -0,0 +1,167 @@ + 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', + ), + ); +} + +/** + * Implementation of hook_handler_info(). + */ +function handlers_test_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', + ) + ), + ); +} + +/** + * Interface for the example string mangling handler. + */ +interface FancystringInterface extends HandlerInterface { + +/** + * Set the string for this object to mangle. + * @param $string + * The string to mangle. + */ + public function setString($string); + + /** + * Get the string back, mangled according to this handler. + * @return + * The mangled string. + */ + public function getString(); +} + +/** + * Base implementation of Fancystring handler. + */ +abstract class FancystringBase implements FancystringInterface { + + protected $target; + + protected $string = ''; + + function __construct($target = 'default') { + $this->target = $target; + } + + public function setString($string) { + $this->string = $string; + } +} + +/** + * Default implementation of Fancystring handler. + */ +class FancystringDefault extends FancystringBase { + + public function getString() { + return $this->string; + } +} + +/** + * Rot13 implementation of Fancystring handler. + */ +class FancystringRot13 extends FancystringBase { + + public function getString() { + return str_rot13($this->string); + } +} + +/** + * Custom replacement implementation of Fancystring handler. + * + * Note that we are implementing the FancystringInterface here directly + * rather than inheriting from the base class, mostly to show that we can. + * There's no obligation that handlers do anything other than support the + * appropriate interface. How they do so is entirely up to them. + * + */ +class FancystringCustom implements FancystringInterface { + + protected $string = ''; + + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } + + public function setString($string) { + $this->string = $string; + } + + public function getString() { + $map = variable_get('fancystring_translate', array()); + + return strtr($this->string, $map); + } +} + +/** + * Interface for the do-nothing test handler. + */ +interface ThingieInterface extends HandlerInterface { + + /** + * Do nothing. + */ + function noop(); +} + +class ThingieDefault implements ThingieInterface { + + protected $target; + + function __construct($target = 'default') { + $this->target = $target; + } + + function noop() { + return; + } +} Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.17 diff -u -p -r1.17 system.api.php --- modules/system/system.api.php 31 Jan 2009 16:50:57 -0000 1.17 +++ modules/system/system.api.php 6 Feb 2009 14:55:26 -0000 @@ -212,7 +212,7 @@ function hook_js_alter(&$javascript) { * $page['content']['nodes'][$nid]['#node'] * // The results pager. * $page['content']['pager'] - * @code + * @code * * Blocks may be referenced by their module/delta pair within a region: * @code @@ -1636,5 +1636,142 @@ function hook_disable() { } /** + * 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". */ Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.306 diff -u -p -r1.306 system.install --- modules/system/system.install 3 Feb 2009 12:30:14 -0000 1.306 +++ modules/system/system.install 6 Feb 2009 14:55:26 -0000 @@ -726,6 +726,116 @@ 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, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target.', + '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 +3323,128 @@ 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, + ), + 'class' => array( + 'description' => 'The PHP class for the handler attached to this slot/target.', + '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. */