Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.268 diff -u -p -r1.268 bootstrap.inc --- includes/bootstrap.inc 25 Jan 2009 12:19:31 -0000 1.268 +++ includes/bootstrap.inc 26 Jan 2009 04:35:29 -0000 @@ -1615,3 +1615,82 @@ function registry_rebuild() { /** * @} End of "ingroup registry". */ + +/** + * Returns the global environment variable. + * + * Technically this is a singleton, which is bad, but we have to break out + * of the dependency injection loop sometime. + * + * @ingroup environment + * @param $reset + * Set TRUE to force the environment variable to be regenerated. + * @return + * A Drupal environment object. + */ +function env($reset = FALSE) { + static $env; + + if (empty($env) || $reset) { + $env = new EnvironmentDefault(); + } + return $env; +} + +/** + * @ingroup handlers + * @{ + */ + +/** + * Main handler entry-point. + * + * This function should be called in most instances in place of a factory + * function. It will automatically route the request to the appropriate + * factory. + * + * @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') { + require_once DRUPAL_ROOT . '/includes/handlers.inc'; + $slot = slot_load($slot_id); + if ($slot) { + $function = $slot->factory; + if (drupal_function_exists($function)) { + return $function($target, $slot_id); + } + } + + return NULL; +} + +/** + * 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 $env + * The environment object this handler should use for interacting with + * the rest of Drupal. + * @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(DrupalEnvironmentInterface $env, $target = 'default'); +} + +/** + * @} End of "ingroup handlers". + */ Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.856 diff -u -p -r1.856 common.inc --- includes/common.inc 23 Jan 2009 14:23:27 -0000 1.856 +++ includes/common.inc 26 Jan 2009 04:35:30 -0000 @@ -4037,6 +4037,7 @@ function drupal_flush_all_caches() { _drupal_flush_css_js(); registry_rebuild(); + handlers_rebuild(); drupal_clear_css_cache(); drupal_clear_js_cache(); @@ -4079,3 +4080,70 @@ 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(); +} + +/** + * Asociate 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); + + 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, + )) + ->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') { + db_delete('handler_attachments') + ->condition('slot', $slot_id) + ->condition('target', $target) + ->execute(); +} + + +/** + * @} End of "ingroup handlers". + */ Index: includes/environment-default.inc =================================================================== RCS file: includes/environment-default.inc diff -N includes/environment-default.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/environment-default.inc 26 Jan 2009 04:35:30 -0000 @@ -0,0 +1,328 @@ +env = $env; + * } + * + * public function otherMethod($string) { + * // Incorrect + * $var = variable_get('myvar', 'default'); + * + * // Correct + * $var = $this->env->variableGet('myvar', 'default'); + * + * // Incorrect + * $param = isset($_GET['page']) ? $_GET['page'] : 0; + * + * // Correct + * $param = $this->env->httpGet('page', 0); + * + * // Incorrect + * $result = db_query("SELECT title FROM {node} WHERE nid= :nid", array(':nid' => 5)); + * + * // Correct + * $result = $this->env->db()->query("SELECT title FROM {node} WHERE nid= :nid", array(':nid' => 5)); + * } + * } + * @endcode + * + * If it is necessary to actively request the environment object, do not instantiate + * a new one. Instead, use the env() factory function. + * + */ + +/** + * The basic Drupal environment object interface. + */ +interface DrupalEnvironmentInterface { + + /** + * Return a persistent variable. + * + * @param $name + * The name of the variable to return. + * @param $default + * The default value to use if this variable has never been set. + * @return + * The value of the variable. + */ + public function variableGet($var, $default); + + /** + * Set a persistent variable. + * + * @param $name + * The name of the variable to set. + * @param $value + * The value to set. This can be any PHP data type; these routines take care + * of serialization as necessary. + */ + public function variableSet($var, $default); + + /** + * Return a variable from the HTTP GET directive. + * + * @see http://www.php.net/manual/en/reserved.variables.get.php + * @param $key + * The value from the GET query to return. If not specified, the entire + * array will be returned. + * @param $default + * The default value to use if the specified key was not sent. + * @return + * The value from the GET array. + */ + public function httpGet($key = NULL, $default = NULL); + + /** + * Return a variable from the HTTP POST directive. + * + * @see http://www.php.net/manual/en/reserved.variables.post.php + * @param $key + * The value from the POST query to return. If not specified, the entire + * array will be returned. + * @param $default + * The default value to use if the specified key was not sent. + * @return + * The value from the POST array. + */ + public function httpPost($key = NULL, $default = NULL); + + /** + * Return a variable from HTTP cookies. + * + * @see http://www.php.net/manual/en/reserved.variables.cookie.php + * @param $key + * The cookie name to return. If not specified, the entire + * array will be returned. + * @param $default + * The default value to use if the specified key was not sent. + * @return + * The value from the COOKIE array. + */ + public function httpCookie($key = NULL, $default = NULL); + + /** + * Return a single argument from the menu query string. + * @param $arg + * The 0-based index of the argument to return. + * @return + * The argument element as a string, or NULL if it was not provided. + */ + public function requestArgument($arg); + + /** + * Return the entire original menu query string. + * + * @return + * The value originally in $_GET['q']. + */ + public function requestPath(); + + /** + * Return a property about the server and request configuration. + * + * This is a wrapper for the $_SERVER superglobal. + * + * @see http://www.php.net/manual/en/reserved.variables.server.php + * @param $key + * The value from the $_SERVER superglobal. If not specified, the entire + * array will be returned. + * @param $default + * The default value to use if the specified key was not sent. + * @return + * The value from the SERVER array. + */ + public function serverInfo($key = NULL, $default = NULL); + + /** + * Return a property about the server and request configuration. + * + * This is a wrapper for the $_ENV superglobal. + * + * @see http://www.php.net/manual/en/reserved.variables.environment.php + * @param $key + * The value from the $_ENV superglobal. If not specified, the entire + * array will be returned. + * @param $default + * The default value to use if the specified key was not sent. + * @return + * The value from the ENV array. + */ + public function envInfo($key = NULL, $default = NULL); + + /** + * Invoke a hook in all enabled modules that implement it. + * + * @param $hook + * The name of the hook to invoke. + * @param ... + * Arguments to pass to the hook. + * @return + * An array of return values of the hook implementations. If modules return + * arrays from their implementations, those are merged into one array. + */ + public function invokeHooks($hook); + + /** + * Request a handler object. + * + * @param $slot + * The slot for which we want a handler. + * @param $options + * An array of options to help determine which handler we want. + * @return + * The appropriate handler object. + */ + public function handler($slot, $options = array()); + + /** + * Gets the connection object for the specified database key and target. + * + * In most cases, calling this method with no parameters is sufficient, + * as that will return a connection object for the active database, default + * target (master server). + * + * @param $target + * The database target to request. Default is "default". + * @param $key + * The key of the connection to reuqest. If not specified, + * uses whatever the currently active database is. + * @return + * The corresponding connection object. + */ + public function db($target = 'default', $key = NULL); +} + +/** + * The default Drupal environment object. + * + * This is the default implementation, intended for "live" usage. In the vast + * majority of cases this is the only version that will be needed for + * production sites. However, unit tests can provide alternate implementations + * as needed. + */ +class EnvironmentDefault implements DrupalEnvironmentInterface { + + protected $requestPath; + protected $getArray; + protected $postArray; + protected $cookieArray; + protected $serverArray; + protected $envArray; + + public function __construct() { + $this->requestPath = drupal_get_normal_path($_GET['q']); + + $this->getArray = $_GET; + $this->postArray = $_POST; + $this->cookieArray = $_COOKIE; + $this->serverArray = $_SERVER; + $this->envArray = $_ENV; + unset($this->getArray['q']); + + $this->requestArguments = explode('/', $this->requestPath); + + } + + public function variableGet($var, $default) { + return variable_get($var, $default); + } + + public function variableSet($var, $default) { + return variable_set($var, $default); + } + + public function requestPath() { + return $this->requestPath; + } + + protected function arrayIndexLookup($array, $key = NULL, $default = NULL) { + if (isset($key)) { + return isset($this->$array[$key]) ? $this->$array[$key] : $default; + } + else { + return $this->$array; + } + } + + public function httpGet($key = NULL, $default = NULL) { + return $this->arrayIndexLookup('getArray', $key, $default); + } + + public function httpPost($key = NULL, $default = NULL) { + return $this->arrayIndexLookup('postArray', $key, $default); + } + + public function httpCookie($key = NULL, $default = NULL) { + return $this->arrayIndexLookup('cookieArray', $key, $default); + } + + public function serverInfo($key = NULL, $default = NULL) { + return $this->arrayIndexLookup('serverArray', $key, $default); + } + + public function envInfo($key = NULL, $default = NULL) { + return $this->arrayIndexLookup('envArray', $key, $default); + } + + public function requestArgument($arg) { + return isset($this->requestArguments[$arg]) ? $this->requestArguments[$arg] : NULL; + } + + public function invokeHooks($hook) { + return call_user_func_array('module_invoke_all', func_get_args()); + } + + public function handler($slot, $target = 'default') { + return handler($slot, $target); + } + + public function db($target = 'default', $key = NULL) { + if (empty($key)) { + $key = 'default'; + } + return Database::getConnection($key, $target); + } + +} + +/** + * @} End of "ingroup environment". + */ 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 26 Jan 2009 04:35:30 -0000 @@ -0,0 +1,291 @@ +someMethod(); + * @endcode + * + * If no handler has been attached to that slot and target, the default handler + * specified by the slot definition 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', 'factory', 'default_handler', 'description')); + + foreach ($slots as $slot => $info) { + $insert->values(array( + 'slot' => $slot, + 'title' => $info['title'], + 'interface' => $info['interface'], + 'factory' => $info['factory'], + '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 ; +} + +/** + * Define various default values for handler slots. + * + * @return + * The default "empty" slot definition. + */ +function handler_slot_defaults() { + return array( + 'slot' => '', + 'title' => '', + 'interface' => 'HandlerInterface', + 'factory' => 'handler_factory_default', + '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, factory, 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]; +} + +/** + * The generic handler factory. + * + * In most cases, this factory will be sufficient for handlers that do not + * need a new object generated on each call. + * + * The default factory function will statically cache handlers so that the same + * object is returned for each call to the same slot/target pair. + * + * The $target and $slot_id parameters are in reverse order so that alternate + * factory functions for specific slots can ignore the $slot_id (which they + * will already know) and still get the $target, which is always important. + * + * @param $target + * The target determines which handler object should be used, depending + * on the current configuration. + * @param $slot_id + * The slot we are handling. + * @return + * The handler object required. + */ +function handler_factory_default($target, $slot_id) { + static $handlers; + + if (empty($handlers[$slot_id][$target])) { + + // Try to get the associated handler. + $class = db_query("SELECT class FROM {handler_attachments} WHERE slot = :slot AND target = :target", array( + 'slot' => $slot_id, + ':target' => $target, + ))->fetchField(); + + // If NULL was returned, there is no handler associated for that target. + // Instead, use the default handler's class. If that fails too, then + // the slot is improperly defined so we don't care. + if (!$class) { + $class = db_query("SELECT class + FROM {handler_info} h + INNER JOIN {handler_slot_info} s + ON h.handler = s.default_handler + WHERE h.slot = :slot", array(':slot' => $slot_id))->fetchField(); + } + + $handlers[$slot_id][$target] = new $class(env(), $target); + } + + return $handlers[$slot_id][$target]; +} + +/** + * @} End of "ingroup handlers". + */ Index: modules/simpletest/tests/environment.test =================================================================== RCS file: modules/simpletest/tests/environment.test diff -N modules/simpletest/tests/environment.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/environment.test 26 Jan 2009 04:35:30 -0000 @@ -0,0 +1,65 @@ + t('Default environment'), + 'description' => t('Test the default environment object'), + 'group' => t('Handlers'), + ); + } + + function setUp() { + parent::setUp(); + + // To unit test alternate implementations, subclass this test class, + // override this name, and run it. That applies the same tests to any + // implementation. + $this->environmentName = 'EnvironmentDefault'; + + } + + /** + * Test that the env() utility function works. + */ + function testEnv() { + + $env1 = env(); + $env2 = env(); + + $env3 = env(TRUE); + + $this->assertTrue($env1 instanceof DrupalEnvironmentInterface, t('First returned object has correct interface.')); + $this->assertTrue($env2 instanceof DrupalEnvironmentInterface, t('Second returned object has correct interface.')); + $this->assertTrue($env3 instanceof DrupalEnvironmentInterface, t('Third returned object has correct interface.')); + + $this->assertIdentical($env1, $env2, t('First two requests returned the same environment object.')); + $this->assertNotIdentical($env1, $env3, t('Reset parameter worked correctly.')); + } + + /** + * Test that we can access page arguments properly. + */ + function testArguments() { + + $env = new $this->environmentName; + + $this->assertEqual($env->requestArgument(0), arg(0), t('Correct argument returned.')); + } + + /** + * Test that we can connect to the database through the environment object. + */ + function testDatabase() { + + $env = new $this->environmentName; + + $this->assertIdentical($env->db(), Database::getConnection('default', 'default'), t('Correct database connection retrieved.')); + } +} + 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 26 Jan 2009 04:35:30 -0000 @@ -0,0 +1,198 @@ +overrides[$var])) { + return $this->overrides[$var]; + } + + return variable_get($var, $default); + } + + /** + * Override variableSet to control the variable table. + * + * @see includes/EnvironmentDefault#variableSet() + */ + public function variableSet($var, $default) { + $this->overrides[$var] = $default; + } +} + +/** + * Test class for the Handlers system. + */ +class HandlersTestCase extends DrupalWebTestCase { + + protected $sampleString = 'Hello World'; + + function getInfo() { + return array( + 'name' => t('Handlers'), + '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 specific handler using a mocked environment object. + * + * This doesn't test the handler routing system, but does demonstrate how + * the environment object is useful for testing. + */ + function testMockEnvironmentHandler() { + + $map = array( + 'Hello' => 'Goodbye', + 'World' => 'Cruel World', + ); + + $env = new EnvironmentVariableOverride(); + $env->variableSet('fancystring_translate', $map); + + $handler = new FancystringCustom($env); + + $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.')); + } + } +} + +/** + * Test class for the Handlers system, part 2. + */ +class Handlers2TestCase extends DrupalWebTestCase { + + protected $sampleString = 'Hello World'; + + function getInfo() { + return array( + 'name' => t('Handlers 2'), + 'description' => t('Test the Handler routing system, part 2'), + '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 successfully unset a handler attachment. + * + * We have to break this to a separate unit test class to avoid issues with + * static caching in the default factory. + */ + 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.')); + } + } +} + 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 26 Jan 2009 04:35:30 -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 26 Jan 2009 04:35:30 -0000 @@ -0,0 +1,144 @@ + array( + 'title' => 'Fancy string', + 'description' => 'Do fancy stuff to strings', + 'interface' => 'FancystringInterface', + 'default_handler' => 'default', + ), + 'other' => array( + 'title' => 'Other handler', + 'description' => 'Dummy', + 'targets' => array(), + 'interface' => 'OtherInterface', + //'factory' => 'other', + ), + + ); +} + +/** + * 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.', + ), + ), + ); +} + +/** + * 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 $env; + +protected $target; + + protected $string = ''; + + function __construct(DrupalEnvironmentInterface $env, $target = 'default') { + $this->env = $env; + $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 $env; + + protected $string = ''; + + protected $target; + + function __construct(DrupalEnvironmentInterface $env, $target = 'default') { + $this->env = $env; + $this->target = $target; + } + + public function setString($string) { + $this->string = $string; + } + + public function getString() { + $map = $this->env->variableGet('fancystring_translate', array()); + + return strtr($this->string, $map); + } +} Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.13 diff -u -p -r1.13 system.api.php --- modules/system/system.api.php 17 Jan 2009 20:28:46 -0000 1.13 +++ modules/system/system.api.php 26 Jan 2009 04:35:31 -0000 @@ -1382,14 +1382,14 @@ function hook_schema_alter(&$schema) { * * @see hook_query_TAG_alter() * @see node_query_node_access_alter() - * + * * @param $query * A Query object describing the composite parts of a SQL query. * @return * None. */ function hook_query_alter(QueryAlterableInterface $query) { - + } /** @@ -1397,7 +1397,7 @@ function hook_query_alter(QueryAlterable * * @see hook_query_alter() * @see node_query_node_access_alter() - * + * * @param $query * An Query object describing the composite parts of a SQL query. * @return @@ -1430,7 +1430,7 @@ function hook_query_TAG_alter(QueryAlter if (count($or->conditions())) { $query->condition($or); } - + $query->condition("{$access_alias}.grant_$op", 1, '>='); } } @@ -1577,5 +1577,140 @@ 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. + * factory (optional) + * The name of a function that serves as the factory for this slot. The factory + * is the function that is called to return the appropriate handler for a + * slot and target. In most cases it will not be called directly but will + * be sub-called from handler() automatically. If not specified, the default + * factory function is handler_factory_default(). Use a custom factory if + * you need different behavior than the default factory with regards to + * caching, reuse of handlers, etc. Factory functions must have the same + * signature as handler_factory_default(). + * + * 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', + ), + 'other' => array( + 'title' => 'Other handler', + 'description' => 'Dummy', + 'targets' => array(), + 'interface' => 'OtherInterface', + 'factory' => 'custom_function', + ), + ); +} + +/** + * Alter the slot definitions of another module. + * + * @param $info + * The complete slot definition array from hook_slot_info(). + */ +function hook_slot_info_alter(&$info) { +// Set a custom factory for the fancystring slot. + $info['fancystring']['factory'] = '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.', + ), + ), + ); +} + +/** + * 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.304 diff -u -p -r1.304 system.install --- modules/system/system.install 25 Jan 2009 16:46:05 -0000 1.304 +++ modules/system/system.install 26 Jan 2009 04:35:31 -0000 @@ -729,6 +729,110 @@ 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, + ), + ), + '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, + ), + 'factory' => array( + 'description' => 'The factory function used to generate new handlers for this slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + '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( @@ -3216,6 +3320,122 @@ 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, + ), + ), + '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, + ), + 'factory' => array( + 'description' => 'The factory function used to generate new handlers for this slot.', + 'type' => 'varchar', + 'length' => '100', + 'not null' => TRUE, + ), + '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. */