Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.190 diff -u -p -r1.190 install.php --- install.php 30 Jul 2009 19:32:19 -0000 1.190 +++ install.php 9 Aug 2009 14:08:14 -0000 @@ -248,6 +248,7 @@ function install_begin_request(&$install // Load module basics (needed for hook invokes). include_once DRUPAL_ROOT . '/includes/module.inc'; include_once DRUPAL_ROOT . '/includes/session.inc'; + include_once DRUPAL_ROOT . '/includes/entity.inc'; $module_list['system']['filename'] = 'modules/system/system.module'; $module_list['filter']['filename'] = 'modules/filter/filter.module'; module_list(TRUE, FALSE, $module_list); Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.956 diff -u -p -r1.956 common.inc --- includes/common.inc 8 Aug 2009 20:52:32 -0000 1.956 +++ includes/common.inc 9 Aug 2009 14:08:17 -0000 @@ -4917,3 +4917,91 @@ function _drupal_flush_css_js() { } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * Get the entity info array of an entity type. + * + * @see hook_entity_info() + * @see hook_entity_info_alter() + * + * @param $entity_type + * The entity type, e.g. node, for which the info shall be returned, or NULL + * to return an array with info about all types. + */ +function drupal_get_entity_info($entity_type = NULL) { + // We statically cache the information returned by hook_entity_info(). + $entity_info = &drupal_static(__FUNCTION__, array()); + + if (empty($entity_info)) { + if ($cache = cache_get('entity_info')) { + $entity_info = $cache->data; + } + else { + $entity_info = module_invoke_all('entity_info'); + // Merge in default values. + foreach ($entity_info as $name => $data) { + $entity_info[$name] += array( + 'fieldable' => FALSE, + 'loader class' => 'DrupalDefaultEntityLoader', + 'static cache' => TRUE, + ); + } + // Let other modules alter the entity info. + drupal_alter('entity_info', $entity_info); + cache_set('entity_info', $entity_info); + } + } + + return empty($entity_type) ? $entity_info : $entity_info[$entity_type]; +} + +/** + * Load entities from the database. + * + * This function should be used whenever you need to load more than one entity + * from the database. The entities are loaded into memory and will not require + * database access if loaded again during the same page request. + * + * The actual loading is done through a class that has to implement the + * DrupalEntityLoader interface. By default, DrupalDefaultEntityLoader is used. + * Entity types can specify that a different class should be used by setting + * the 'loader class' key in hook_entity_info(). These classes can either + * implement the DrupalEntityLoader interface, or, most commonly, extend the + * DrupalDefaultEntityLoader class. See node_entity_info() and the class + * NodeLoader in node.module as an example. + * + * @see hook_entity_info() + * @see DrupalEntityLoader + * @see DrupalDefaultEntityLoader + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * An array of conditions on the entity base table in the form 'field' => $value. + * @param $reset + * Whether to reset the internal cache for the requested entity type. + * + * @return + * An array of entity objects indexed by their ids. + */ +function entity_load_multiple($entity_type, $ids = array(), $conditions = array(), $reset = FALSE) { + if ($reset) { + entity_get_loader($entity_type)->resetCache(); + } + return entity_get_loader($entity_type)->loadMultiple($ids, $conditions); +} + +/** + * Get the entity loader class for an entity type. + */ +function entity_get_loader($entity_type) { + $loaders = &drupal_static(__FUNCTION__, array()); + if (!isset($loaders[$entity_type])) { + $type_info = drupal_get_entity_info($entity_type); + $class = $type_info['loader class']; + $loaders[$entity_type] = new $class($entity_type); + } + return $loaders[$entity_type]; +} Index: includes/entity.inc =================================================================== RCS file: includes/entity.inc diff -N includes/entity.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/entity.inc 9 Aug 2009 14:08:20 -0000 @@ -0,0 +1,281 @@ + $value. + * + * @return + * An array of entity objects indexed by their ids. + */ + public function loadMultiple($ids = array(), $conditions = array()); +} + +/** + * Default implementation of DrupalEntityLoaderInterface. + * + * This class can be used as-is by most simple entity types. Entity types + * requiring special handling can extend the class. + */ +class DrupalDefaultEntityLoader implements DrupalEntityLoaderInterface { + + protected $entityCache; + protected $entityType; + protected $entityInfo; + protected $hookLoadArguments; + protected $idKey; + protected $revisionKey; + protected $revisionTable; + protected $query; + + /** + * Constructor. Set basic variables. + */ + public function __construct($entityType) { + $this->entityType = $entityType; + $this->entityInfo = drupal_get_entity_info($entityType); + $this->entityCache = array(); + $this->hookLoadArguments = array(); + $this->idKey = $this->entityInfo['object keys']['id']; + + // Check if the entity type supports revisions. + if (isset($this->entityInfo['object keys']['revision'])) { + $this->revisionKey = $this->entityInfo['object keys']['revision']; + $this->revisionTable = $this->entityInfo['revision table']; + } + else { + $this->revisionKey = FALSE; + } + + // Check if the entity type supports static caching of loaded entities. + $this->cache = !empty($this->entityInfo['static cache']); + } + + public function resetCache() { + $this->entityCache = array(); + } + + public function loadMultiple($ids = array(), $conditions = array()) { + $this->ids = $ids; + $this->conditions = $conditions; + + $entities = array(); + + // Revisions are not statically cached, and require a different query to + // other conditions, so separate the revision id into its own variable. + if ($this->revisionKey && isset($this->conditions[$this->revisionKey])) { + $this->revisionId = $this->conditions[$this->revisionKey]; + unset($this->conditions[$this->revisionKey]); + } + else { + $this->revisionId = FALSE; + } + + + // Create a new variable which is either a prepared version of the $ids + // array for later comparison with the entity cache, or FALSE if no $ids were + // passed. The $ids array is reduced as items are loaded from cache, and we + // need to know if it's empty for this reason to avoid querying the database + // when all requested entities are loaded from cache. + $passed_ids = !empty($this->ids) ? array_flip($this->ids) : FALSE; + // Try to load entities from the static cache, if the entity type supports + // static caching. + if ($this->cache) { + $entities += $this->cacheGet($this->ids, $this->conditions); + // If any entities were loaded, remove them from the ids still to load. + if ($passed_ids) { + $this->ids = array_keys(array_diff_key($passed_ids, $entities)); + } + } + + // Load any remaining entities from the database. This is the case if $ids + // is set to FALSE (so we load all entities), if there are any ids left to + // load, if loading a revision, or if $conditions was passed without $ids. + if ($this->ids === FALSE || $this->ids || $this->revisionId || ($this->conditions && !$passed_ids)) { + // Build the query. + $this->buildQuery(); + $queried_entities = $this->query + ->execute() + ->fetchAllAssoc($this->idKey); + } + + // Pass all entities loaded from the database through $this->attachLoad(), + // which attaches fields (if supported by the entity type) and calls the + // entity type specific load callback. + if (!empty($queried_entities)) { + $this->attachLoad($queried_entities); + $entities += $queried_entities; + } + + if ($this->cache) { + // Add entities to the cache if we're not loading a revision. + if (!empty($queried_entities) && !$this->revisionId) { + $this->cacheSet($queried_entities); + } + // Ensure that the returned array is ordered the same as the original $ids + // array if this was passed in and remove any invalid ids. + if ($passed_ids) { + // Remove any invalid ids from the array. + $passed_ids = array_intersect_key($passed_ids, $entities); + foreach ($entities as $entity) { + $passed_ids[$entity->{$this->idKey}] = $entity; + } + $entities = $passed_ids; + } + } + + return $entities; + } + + /** + * Build the query to load the entity. + * + * This has full revision support. For entities requiring special queries, + * the class can be extendend, and the default query can be constructed by + * calling parent::buildQuery(). See NodeLoader::buildQuery() for an example. + */ + protected function buildQuery() { + $this->query = db_select($this->entityInfo['base table'], 'base'); + + $this->query->addTag($this->entityType . '_load_multiple'); + + if ($this->revisionId) { + $this->query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $this->revisionId)); + } + elseif ($this->revisionKey) { + $this->query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}"); + } + + // Add fields from the {entity} table. + $entity_fields = drupal_schema_fields_sql($this->entityInfo['base table']); + + if ($this->revisionKey) { + // Add all fields from the {entity_revision} table. + $entity_revision_fields = drupal_schema_fields_sql($this->revisionTable); + // The id field is provided by entity, so remove it. + unset($entity_revision_fields[$this->idKey]); + + // Change timestamp to revision_timestamp before adding it to the query. + // TODO: This is node specific and has to be moved into NodeLoader. + unset($entity_revision_fields['timestamp']); + $this->query->addField('revision', 'timestamp', 'revision_timestamp'); + + // Remove all fields from the base table that are also fields by the same + // name in the revision table. + $entity_field_keys = array_flip($entity_fields); + foreach ($entity_revision_fields as $key => $name) { + if (isset($entity_field_keys[$name])) { + unset($entity_fields[$entity_field_keys[$name]]); + } + } + $this->query->fields('revision', $entity_revision_fields); + } + + $this->query->fields('base', $entity_fields); + + if ($this->ids) { + $this->query->condition("base.{$this->idKey}", $this->ids, 'IN'); + } + if ($this->conditions) { + foreach ($this->conditions as $field => $value) { + $this->query->condition('base.' . $field, $value); + } + } + } + + /** + * Attach data to entities upon loading. + * + * This will attach fields, if the entity is fieldable. It also calls + * hook_entityType_load() on the loaded entities. If your hook_entityType_load() + * expects special parameters apart from the queried entities, you can set + * $this->hookLoadArguments prior to calling the method. + * See NodeLoader::attachLoad() for an example. + */ + protected function attachLoad(&$queried_entities) { + // Attach fields. + if ($this->entityInfo['fieldable']) { + if ($this->revisionId) { + field_attach_load_revision($this->entityType, $queried_entities); + } + else { + field_attach_load($this->entityType, $queried_entities); + } + } + + // Call hook_entityType_load(). The first argument for hook_TYPE_load() are + // always the queried entities, followed by additional arguments set in + // $this->hookLoadArguments. + $args = array_merge(array($queried_entities), $this->hookLoadArguments); + foreach (module_implements($this->entityType . '_load') as $module) { + call_user_func_array($module . '_' . $this->entityType . '_load', $args); + } + } + + /** + * Get entities from the static cache. + * + * @param $ids + * If not empty, return entities that match these IDs. + * @param $conditions + * If set, return entities that match all of these conditions. + */ + protected function cacheGet($ids, $conditions = array()) { + $entities = array(); + // Load any available entities from the internal cache. + if (!empty($this->entityCache) && !$this->revisionId) { + if ($ids) { + $entities += array_intersect_key($this->entityCache, array_flip($ids)); + } + // If loading entities only by conditions, fetch all available entities from + // the cache. Entities which don't match are removed later. + elseif ($conditions) { + $entities = $this->entityCache; + } + } + + // Exclude any entities loaded from cache if they don't match $conditions. + // This ensures the same behavior whether loading from memory or database. + if ($conditions) { + foreach ($entities as $entity) { + $entity_values = (array) $entity; + if (array_diff_assoc($conditions, $entity_values)) { + unset($entities[$entity->{$this->idKey}]); + } + } + } + return $entities; + } + + /** + * Store entities in the static entity cache. + */ + protected function cacheSet($entities) { + $this->entityCache += $entities; + } +} Index: includes/file.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/file.inc,v retrieving revision 1.179 diff -u -p -r1.179 file.inc --- includes/file.inc 2 Aug 2009 05:43:54 -0000 1.179 +++ includes/file.inc 9 Aug 2009 14:08:20 -0000 @@ -490,34 +490,12 @@ function file_check_location($source, $d * @return * An array of file objects, indexed by fid. * + * @see entity_load_multiple() * @see hook_file_load() * @see file_load() */ function file_load_multiple($fids = array(), $conditions = array()) { - $query = db_select('files', 'f')->fields('f'); - - // If the $fids array is populated, add those to the query. - if ($fids) { - $query->condition('f.fid', $fids, 'IN'); - } - - // If the conditions array is populated, add those to the query. - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('f.' . $field, $value); - } - } - $files = $query->execute()->fetchAllAssoc('fid'); - - // Invoke hook_file_load() on the terms loaded from the database - // and add them to the static cache. - if (!empty($files)) { - foreach (module_implements('file_load') as $module) { - $function = $module . '_file_load'; - $function($files); - } - } - return $files; + return entity_load_multiple('file', $fids, $conditions); } /** Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.158 diff -u -p -r1.158 blogapi.module --- modules/blogapi/blogapi.module 5 Jul 2009 18:00:07 -0000 1.158 +++ modules/blogapi/blogapi.module 9 Aug 2009 14:08:20 -0000 @@ -595,7 +595,7 @@ function blogapi_mt_validate_terms($node } } // Look up all the vocabularies for this node type. - $vocabularies = taxonomy_vocabulary_load_multiple(array(), array('type' => $node->type)); + $vocabularies = taxonomy_vocabulary_load_multiple(FALSE, array('type' => $node->type)); // Check each vocabulary associated with this node type. foreach ($vocabularies as $vocabulary) { // Required vocabularies must have at least one term. Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.751 diff -u -p -r1.751 comment.module --- modules/comment/comment.module 9 Aug 2009 00:55:50 -0000 1.751 +++ modules/comment/comment.module 9 Aug 2009 14:08:22 -0000 @@ -96,6 +96,23 @@ function comment_help($path, $arg) { } /** + * Implement hook_entity_info() { + */ +function comment_entity_info() { + return array( + 'comment' => array( + 'label' => t('Comment'), + 'base table' => 'comment', + 'loader class' => 'CommentLoader', + 'object keys' => array( + 'id' => 'cid', + ), + 'static cache' => FALSE, + ), + ); +} + +/** * Implement hook_theme(). */ function comment_theme() { @@ -1428,47 +1445,7 @@ function comment_operations($action = NU * An array of comment objects, indexed by comment ID. */ function comment_load_multiple($cids = array(), $conditions = array()) { - $comments = array(); - if ($cids || $conditions) { - $query = db_select('comment', 'c'); - $query->innerJoin('users', 'u', 'c.uid = u.uid'); - $query->innerJoin('node', 'n', 'c.nid = n.nid'); - $query->addField('u', 'name', 'registered_name'); - $query->addField('n', 'type', 'node_type'); - $query - ->fields('c', array('cid', 'nid', 'pid', 'comment', 'subject', 'format', 'timestamp', 'name', 'mail', 'homepage', 'status', 'thread')) - ->fields('u', array( 'uid', 'signature', 'picture', 'data', 'status')); - - // If the $cids array is populated, add those to the query. - if ($cids) { - $query->condition('c.cid', $cids, 'IN'); - } - - // If the conditions array is populated, add those to the query. - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('c.' . $field, $value); - } - } - $comments = $query->execute()->fetchAllAssoc('cid'); - } - - // Setup standard comment properties. - foreach ($comments as $key => $comment) { - $comment = drupal_unpack($comment); - $comment->name = $comment->uid ? $comment->registered_name : $comment->name; - $comment->new = node_mark($comment->nid, $comment->timestamp); - $comment->node_type = 'comment_node_' . $comment->node_type; - $comments[$key] = $comment; - } - - if (!empty($comments)) { - // Attach fields. - field_attach_load('comment', $comments); - // Invoke hook_comment_load(). - module_invoke_all('comment_load', $comments); - } - return $comments; + entity_load_multiple('comment', $cids, $conditions); } /** @@ -1485,6 +1462,32 @@ function comment_load($cid) { } /** + * Loader class for comments. + * + * This extends the DrupalDefaultEntityLoader class, adding required special + * handling for comment objects. + */ +class CommentLoader extends DrupalDefaultEntityLoader { + protected function buildQuery() { + parent::buildQuery(); + // Specify additional fields from the user table. + $this->query->innerJoin('users', 'u', 'base.uid = u.uid'); + $this->query->addField('u', 'name', 'registered_name'); + $this->query->fields('u', array( 'uid', 'signature', 'picture', 'data', 'status')); + } + + protected function attachLoad(&$comments) { + // Setup standard comment properties. + foreach ($comments as $key => $comment) { + $comment = drupal_unpack($comment); + $comment->name = $comment->uid ? $comment->registered_name : $comment->name; + $comment->new = node_mark($comment->nid, $comment->timestamp); + $comments[$key] = $comment; + } + } +} + +/** * Get replies count for a comment. * * @param $pid Index: modules/field/field.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v retrieving revision 1.24 diff -u -p -r1.24 field.api.php --- modules/field/field.api.php 1 Aug 2009 06:03:12 -0000 1.24 +++ modules/field/field.api.php 9 Aug 2009 14:08:23 -0000 @@ -2,109 +2,6 @@ // $Id: field.api.php,v 1.24 2009/08/01 06:03:12 dries Exp $ /** - * @ingroup field_fieldable_type - * @{ - */ - -/** - * Expose fieldable object types. - * - * Inform the Field API about object types to which fields can be attached. - * @see hook_fieldable_info_alter(). - * - * @return - * An array whose keys are fieldable object type names and whose values are - * arrays with the following key/value pairs: - * - label: The human-readable name of the type. - * - object keys: An array describing how the Field API can extract the - * informations it needs from the objects of the type. - * - id: The name of the property that contains the primary id of the - * object. Every object passed to the Field API must have this property - * and its value must be numeric. - * - revision: The name of the property that contains the revision id of - * the object. The Field API assumes that all revision ids are unique - * across all objects of a type. - * This element can be omitted if the objects of this type are not - * versionable. - * - bundle: The name of the property that contains the bundle name for the - * object. The bundle name defines which set of fields are attached to - * the object (e.g. what nodes call "content type"). - * This element can be omitted if this type has no bundles (all objects - * have the same fields). - * - bundle keys: An array describing how the Field API can extract the - * informations it needs from the bundle objects for this type (e.g - * $vocabulary objects for terms; not applicable for nodes). - * This element can be omitted if this type's bundles do not exist as - * standalone objects. - * - bundle: The name of the property that contains the name of the bundle - * object. - * - cacheable: A boolean indicating whether Field API should cache - * loaded fields for each object, reducing the cost of - * field_attach_load(). - * - bundles: An array describing all bundles for this object type. - * Keys are bundles machine names, as found in the objects' 'bundle' - * property (defined in the 'object keys' entry above). - * - label: The human-readable name of the bundle. - * - admin: An array of informations that allow Field UI pages (currently - * implemented in a contributed module) to attach themselves to the - * existing administration pages for the bundle. - * - path: the path of the bundle's main administration page, as defined - * in hook_menu(). If the path includes a placeholder for the bundle, - * the 'bundle argument', 'bundle helper' and 'real path' keys below - * are required. - * - bundle argument: The position of the placeholder in 'path', if any. - * - real path: The actual path (no placeholder) of the bundle's main - * administration page. This will be used to generate links. - * - access callback: As in hook_menu(). 'user_access' will be assumed if - * no value is provided. - * - access arguments: As in hook_menu(). - */ -function hook_fieldable_info() { - $return = array( - 'taxonomy_term' => array( - 'label' => t('Taxonomy term'), - 'object keys' => array( - 'id' => 'tid', - 'bundle' => 'vocabulary_machine_name', - ), - 'bundle keys' => array( - 'bundle' => 'machine_name', - ), - 'bundles' => array(), - ), - ); - foreach (taxonomy_get_vocabularies() as $vocabulary) { - $return['taxonomy_term']['bundles'][$vocabulary->machine_name] = array( - 'label' => $vocabulary->name, - 'admin' => array( - 'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary', - 'real path' => 'admin/structure/taxonomy/' . $vocabulary->vid, - 'bundle argument' => 3, - 'access arguments' => array('administer taxonomy'), - ), - ); - } - return $return; -} - -/** - * Perform alterations on fieldable types. - * - * @param $info - * Array of informations on fieldable types exposed by hook_fieldable_info() - * implementations. - */ -function hook_fieldable_info_alter(&$info) { - // A contributed module handling node-level caching would want to disable - // field cache for nodes. - $info['node']['cacheable'] = FALSE; -} - -/** - * @} End of "ingroup field_fieldable_type" - */ - -/** * @defgroup field_types Field Types API * @{ * Define field types, widget types, and display formatter types. Index: modules/field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.11 diff -u -p -r1.11 field.info.inc --- modules/field/field.info.inc 2 Aug 2009 11:24:21 -0000 1.11 +++ modules/field/field.info.inc 9 Aug 2009 14:08:23 -0000 @@ -41,10 +41,11 @@ * * label, field types, behaviors: from hook_field_formatter_info() * * module: module that exposes the formatter type - * fieldable types: array of hook_fieldable_info() results, keyed by entity_type. + * fieldable types: array of hook_entity_info() results, keyed by entity_type. * * name, id key, revision key, bundle key, cacheable, bundles: from - * hook_fieldable_info() + * hook_entity_info() * * module: module that exposes the entity type + * @TODO use drupal_get_entity_info(). */ function _field_info_collate_types($reset = FALSE) { static $info; @@ -111,18 +112,20 @@ function _field_info_collate_types($rese drupal_alter('field_formatter_info', $info['formatter types']); // Populate information about 'fieldable' entities. - foreach (module_implements('fieldable_info') as $module) { - $fieldable_types = (array) module_invoke($module, 'fieldable_info'); + foreach (module_implements('entity_info') as $module) { + $fieldable_types = (array) module_invoke($module, 'entity_info'); foreach ($fieldable_types as $name => $fieldable_info) { - // Provide defaults. - $fieldable_info += array( - 'cacheable' => TRUE, - 'bundles' => array(), - ); - $fieldable_info['object keys'] += array( - 'revision' => '', - 'bundle' => '', - ); + if (!empty($fieldable_info['fieldable'])) { + // Provide defaults. + $fieldable_info += array( + 'cacheable' => TRUE, + 'bundles' => array(), + ); + $fieldable_info['object keys'] += array( + 'revision' => '', + 'bundle' => '', + ); + } // If no bundle key provided, then we assume a single bundle, named // after the type of the object. Make sure the bundle created // has the human-readable name we need for bundle messages. Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.37 diff -u -p -r1.37 field.test --- modules/field/field.test 9 Aug 2009 01:28:06 -0000 1.37 +++ modules/field/field.test 9 Aug 2009 14:08:26 -0000 @@ -579,7 +579,7 @@ class FieldAttachTestCase extends Drupal function testFieldAttachCreateRenameBundle() { // Create a new bundle. This has to be initiated by the module so that its - // hook_fieldable_info() is consistent. + // hook_entity_info() is consistent. $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName()); field_test_create_bundle($new_bundle); @@ -600,7 +600,7 @@ class FieldAttachTestCase extends Drupal $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], "Data are retrieved for the new bundle"); // Rename the bundle. This has to be initiated by the module so that its - // hook_fieldable_info() is consistent. + // hook_entity_info() is consistent. $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName()); field_test_rename_bundle($this->instance['bundle'], $new_bundle); @@ -616,7 +616,7 @@ class FieldAttachTestCase extends Drupal function testFieldAttachDeleteBundle() { // Create a new bundle. This has to be initiated by the module so that its - // hook_fieldable_info() is consistent. + // hook_entity_info() is consistent. $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName()); field_test_create_bundle($new_bundle); @@ -656,7 +656,7 @@ class FieldAttachTestCase extends Drupal $this->assertEqual(count($entity->{$field_name}), 1, "Second field got loaded"); // Delete the bundle. This has to be initiated by the module so that its - // hook_fieldable_info() is consistent. + // hook_entity_info() is consistent. field_test_delete_bundle($this->instance['bundle']); // Verify no data gets loaded Index: modules/forum/forum.test =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.test,v retrieving revision 1.27 diff -u -p -r1.27 forum.test --- modules/forum/forum.test 3 Aug 2009 03:04:33 -0000 1.27 +++ modules/forum/forum.test 9 Aug 2009 14:08:27 -0000 @@ -136,7 +136,7 @@ class ForumTestCase extends DrupalWebTes $this->assertRaw(t('Updated vocabulary %name.', array('%name' => $title)), t('Vocabulary was edited')); // Grab the newly edited vocabulary. - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + drupal_get_entity_loader('taxonomy_vocabulary')->resetCache(); $current_settings = taxonomy_vocabulary_load($vid); // Make sure we actually edited the vocabulary properly. Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1095 diff -u -p -r1.1095 node.module --- modules/node/node.module 8 Aug 2009 22:52:59 -0000 1.1095 +++ modules/node/node.module 9 Aug 2009 14:08:28 -0000 @@ -119,12 +119,16 @@ function node_cron() { } /** - * Implement hook_fieldable_info(). + * Implement hook_entity_info(). */ -function node_fieldable_info() { +function node_entity_info() { $return = array( 'node' => array( 'label' => t('Node'), + 'loader class' => 'NodeLoader', + 'base table' => 'node', + 'revision table' => 'node_revision', + 'fieldable' => TRUE, 'object keys' => array( 'id' => 'nid', 'revision' => 'vid', @@ -177,7 +181,7 @@ function node_field_build_modes($obj_typ * Gather a listing of links to nodes. * * @param $result - * A DB result object from a query to fetch node objects. If your query + * A DB result object from a query to fetch node entities. If your query * joins the node_comment_statistics table so that the * comment_count field is available, a title attribute will * be added to show the number of comments. @@ -673,12 +677,14 @@ function node_invoke($node, $hook, $a2 = } /** - * Load node objects from the database. + * Load node entities from the database. * * This function should be used whenever you need to load more than one node * from the database. Nodes are loaded into memory and will not require * database access if loaded again during the same page request. * + * @see entity_load_multiple() + * * @param $nids * An array of node IDs. * @param $conditions @@ -690,144 +696,7 @@ function node_invoke($node, $hook, $a2 = * An array of node objects indexed by nid. */ function node_load_multiple($nids = array(), $conditions = array(), $reset = FALSE) { - $node_cache = &drupal_static(__FUNCTION__, array()); - - if ($reset) { - $node_cache = array(); - } - $nodes = array(); - - // Create a new variable which is either a prepared version of the $nids - // array for later comparison with the node cache, or FALSE if no $nids were - // passed. The $nids array is reduced as items are loaded from cache, and we - // need to know if it's empty for this reason to avoid querying the database - // when all requested nodes are loaded from cache. - $passed_nids = !empty($nids) ? array_flip($nids) : FALSE; - - // Revisions are not statically cached, and require a different query to - // other conditions, so separate vid into its own variable. - $vid = isset($conditions['vid']) ? $conditions['vid'] : FALSE; - unset($conditions['vid']); - - // Load any available nodes from the internal cache. - if ($node_cache && !$vid) { - if ($nids) { - $nodes += array_intersect_key($node_cache, $passed_nids); - // If any nodes were loaded, remove them from the $nids still to load. - $nids = array_keys(array_diff_key($passed_nids, $nodes)); - } - // If loading nodes only by conditions, fetch all available nodes from - // the cache. Nodes which don't match are removed later. - elseif ($conditions) { - $nodes = $node_cache; - } - } - - // Exclude any nodes loaded from cache if they don't match $conditions. - // This ensures the same behavior whether loading from memory or database. - if ($conditions) { - foreach ($nodes as $node) { - $node_values = (array) $node; - if (array_diff_assoc($conditions, $node_values)) { - unset($nodes[$node->nid]); - } - } - } - - // Load any remaining nodes from the database. This is the case if there are - // any $nids left to load, if loading a revision, or if $conditions was - // passed without $nids. - if ($nids || $vid || ($conditions && !$passed_nids)) { - $query = db_select('node', 'n'); - - if ($vid) { - $query->join('node_revision', 'r', 'r.nid = n.nid AND r.vid = :vid', array(':vid' => $vid)); - } - else { - $query->join('node_revision', 'r', 'r.vid = n.vid'); - } - - // Add fields from the {node} table. - $node_fields = drupal_schema_fields_sql('node'); - - // vid and title are provided by node_revision, so remove them. - unset($node_fields['vid']); - unset($node_fields['title']); - $query->fields('n', $node_fields); - - // Add all fields from the {node_revision} table. - $node_revision_fields = drupal_schema_fields_sql('node_revision'); - - // nid is provided by node, so remove it. - unset($node_revision_fields['nid']); - - // Change timestamp to revision_timestamp before adding it to the query. - unset($node_revision_fields['timestamp']); - $query->addField('r', 'timestamp', 'revision_timestamp'); - $query->fields('r', $node_revision_fields); - - if ($nids) { - $query->condition('n.nid', $nids, 'IN'); - } - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('n.' . $field, $value); - } - } - $queried_nodes = $query->execute()->fetchAllAssoc('nid'); - } - - // Pass all nodes loaded from the database through the node type specific - // callbacks and hook_node_load(), then add them to the internal cache. - if (!empty($queried_nodes)) { - // Create an array of nodes for each content type and pass this to the - // node type specific callback. - $typed_nodes = array(); - foreach ($queried_nodes as $nid => $node) { - $typed_nodes[$node->type][$nid] = $node; - } - - // Call node type specific callbacks on each typed array of nodes. - foreach ($typed_nodes as $type => $nodes_of_type) { - if (node_hook($type, 'load')) { - $function = node_type_get_base($type) . '_load'; - $function($nodes_of_type); - } - } - - // Attach fields. - if ($vid) { - field_attach_load_revision('node', $queried_nodes); - } - else { - field_attach_load('node', $queried_nodes); - } - - // Call hook_node_load(), pass the node types so modules can return early - // if not acting on types in the array. - foreach (module_implements('node_load') as $module) { - $function = $module . '_node_load'; - $function($queried_nodes, array_keys($typed_nodes)); - } - $nodes += $queried_nodes; - // Add nodes to the cache if we're not loading a revision. - if (!$vid) { - $node_cache += $queried_nodes; - } - } - - // Ensure that the returned array is ordered the same as the original $nids - // array if this was passed in and remove any invalid nids. - if ($passed_nids) { - // Remove any invalid nids from the array. - $passed_nids = array_intersect_key($passed_nids, $nodes); - foreach ($nodes as $node) { - $passed_nids[$node->nid] = $node; - } - $nodes = $passed_nids; - } - - return $nodes; + return entity_load_multiple('node', $nids, $conditions, $reset); } /** @@ -846,7 +715,6 @@ function node_load_multiple($nids = arra function node_load($nid, $vid = array(), $reset = FALSE) { $vid = isset($vid) ? array('vid' => $vid) : NULL; $node = node_load_multiple(array($nid), $vid, $reset); - return $node ? $node[$nid] : FALSE; } @@ -3147,3 +3015,30 @@ function node_requirements($phase) { ); return $requirements; } + +/** + * Loader class for nodes. + * + * This extends the DrupalDefaultEntityLoader class, adding required special + * handling for node objects. + */ +class NodeLoader extends DrupalDefaultEntityLoader { + protected function attachLoad(&$nodes) { + // Create an array of nodes for each content type and pass this to the + // object type specific callback. + $typed_nodes = array(); + foreach ($nodes as $id => $object) { + $typed_nodes[$object->type][$id] = $object; + } + + // Call object type specific callbacks on each typed array of nodes. + foreach ($typed_nodes as $node_type => $nodes_of_type) { + if (node_hook($node_type, 'load')) { + $function = node_type_get_base($type) . '_load'; + $function($nodes_of_type); + } + } + $this->hookLoadArguments[] = array_keys($typed_nodes); + parent::attachLoad($nodes); + } +} Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.13 diff -u -p -r1.13 field_test.module --- modules/simpletest/tests/field_test.module 10 Jul 2009 05:58:13 -0000 1.13 +++ modules/simpletest/tests/field_test.module 9 Aug 2009 14:08:29 -0000 @@ -60,7 +60,7 @@ function field_test_menu() { /** * Define a test fieldable entity. */ -function field_test_fieldable_info() { +function field_test_entity_info() { $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle'))); return array( 'test_entity' => array( @@ -72,6 +72,7 @@ function field_test_fieldable_info() { ), 'cacheable' => FALSE, 'bundles' => $bundles, + 'fieldable' => TRUE, ), // This entity type doesn't get form handling for now... 'test_cacheable_entity' => array( @@ -640,4 +641,4 @@ function field_test_memorize($key = NULL function field_test_field_create_field($field) { $args = func_get_args(); field_test_memorize(__FUNCTION__, $args); -} \ No newline at end of file +} Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.169 diff -u -p -r1.169 system.admin.inc --- modules/system/system.admin.inc 5 Aug 2009 19:40:55 -0000 1.169 +++ modules/system/system.admin.inc 9 Aug 2009 14:08:31 -0000 @@ -953,6 +953,7 @@ function system_modules_submit($form, &$ drupal_theme_rebuild(); node_types_rebuild(); cache_clear_all('schema', 'cache'); + cache_clear_all('entity_info', 'cache'); drupal_clear_css_cache(); drupal_clear_js_cache(); Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.59 diff -u -p -r1.59 system.api.php --- modules/system/system.api.php 8 Aug 2009 22:52:59 -0000 1.59 +++ modules/system/system.api.php 9 Aug 2009 14:08:34 -0000 @@ -12,6 +12,110 @@ */ /** + * Inform the base system and the Field API about one or more entity types. + * + * Inform the system about one or more entity types (i.e., object types that can + * be loaded via entity_load_multiple() and, optionally, to which fields can be + * attached). + * + * @see entity_load_multiple() + * @see hook_entity_info_alter() + * + * @return + * An array whose keys are entity type names and whose values identify + * properties of those types that the system needs to know about: + * + * name: The human-readable name of the type. + * loader class: The name of the class that is used to load the objects. + * The class has to implement the DrupalEntityLoader interface. Leave blank + * to use the DefaultDrupalEntityLoader implementation. + * base table: (used by DefaultDrupalEntityLoader) The name of the entity + * type's base table. + * static cache: (used by DefaultDrupalEntityLoader) FALSE to disable static + * caching of loaded entities during a page request. Defaults to TRUE. + * fieldable: Set to TRUE if you want your entity type to be fieldable. + * - object keys: An array describing how the Field API can extract the + * information it needs from the objects of the type. + * - id: The name of the property that contains the primary id of the + * object. Every object passed to the Field API must have this property + * and its value must be numeric. + * - revision: The name of the property that contains the revision id of + * the object. The Field API assumes that all revision ids are unique + * across all objects of a type. + * This element can be omitted if the objects of this type are not + * versionable. + * - bundle: The name of the property that contains the bundle name for the + * object. The bundle name defines which set of fields are attached to + * the object (e.g. what nodes call "content type"). + * This element can be omitted if this type has no bundles (all objects + * have the same fields). + * - bundle keys: An array describing how the Field API can extract the + * information it needs from the bundle objects for this type (e.g + * $vocabulary objects for terms; not applicable for nodes). + * This element can be omitted if this type's bundles do not exist as + * standalone objects. + * - bundle: The name of the property that contains the name of the bundle + * object. + * cacheable: A boolean indicating whether Field API should cache + * loaded fields for each object, reducing the cost of + * field_attach_load(). + * - bundles: An array describing all bundles for this object type. + * Keys are bundles machine names, as found in the objects' 'bundle' + * property (defined in the 'object keys' entry above). + * - label: The human-readable name of the bundle. + * - admin: An array of information that allow Field UI pages (currently + * implemented in a contributed module) to attach themselves to the + * existing administration pages for the bundle. + * - path: the path of the bundle's main administration page, as defined + * in hook_menu(). If the path includes a placeholder for the bundle, + * the 'bundle argument', 'bundle helper' and 'real path' keys below + * are required. + * - bundle argument: The position of the placeholder in 'path', if any. + * - real path: The actual path (no placeholder) of the bundle's main + * administration page. This will be used to generate links. + * - access callback: As in hook_menu(). 'user_access' will be assumed if + * no value is provided. + * - access arguments: As in hook_menu(). + */ +function hook_entity_info() { + $return = array( + 'node' => array( + 'name' => t('Node'), + 'loader class' => 'NodeLoader', + 'base table' => 'node', + 'id key' => 'nid', + 'revision key' => 'vid', + 'fieldable' => TRUE, + 'bundle key' => 'type', + // Node.module handles its own caching. + // 'cacheable' => FALSE, + // Bundles must provide human readable name so + // we can create help and error messages about them. + 'bundles' => node_type_get_names(), + ), + ); + return $return; +} + +/** + * Alter the entity info. + * + * Modules may implement this hook to alter the information that defines an + * entity. All properties that are available in hook_entity_info() can be + * altered here. + * + * @see hook_entity_info() + * + * @param $entity_info + * The entity info array, keyed by entity name. + */ +function hook_entity_info_alter(&$entity_info) { + // Set the loader class for nodes to an alternate implementation of the + // DrupalEntityLoader interface. + $entity_info['node']['loader class'] = 'MyCustomNodeLoader'; +} + +/** * Perform periodic actions. * * Modules that require to schedule some commands to be executed at regular Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.365 diff -u -p -r1.365 system.install --- modules/system/system.install 8 Aug 2009 20:52:32 -0000 1.365 +++ modules/system/system.install 9 Aug 2009 14:08:34 -0000 @@ -1422,7 +1422,6 @@ function system_update_last_removed() { return 6049; } - /** * @defgroup updates-6.x-to-7.x System updates from 6.x to 7.x * @{ Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.737 diff -u -p -r1.737 system.module --- modules/system/system.module 5 Aug 2009 19:40:55 -0000 1.737 +++ modules/system/system.module 9 Aug 2009 14:08:36 -0000 @@ -255,6 +255,22 @@ function system_rdf_namespaces() { } /** + * Implement hook_entity_info(). + */ +function system_entity_info() { + return array( + 'file' => array( + 'label' => t('File'), + 'base table' => 'files', + 'object keys' => array( + 'id' => 'fid', + ), + 'static cache' => FALSE, + ), + ); +} + +/** * Implement hook_elements(). */ function system_elements() { Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.493 diff -u -p -r1.493 taxonomy.module --- modules/taxonomy/taxonomy.module 4 Aug 2009 06:50:07 -0000 1.493 +++ modules/taxonomy/taxonomy.module 9 Aug 2009 14:08:38 -0000 @@ -19,12 +19,15 @@ function taxonomy_permission() { } /** - * Implement hook_fieldable_info(). + * Implement hook_entity_info(). */ -function taxonomy_fieldable_info() { +function taxonomy_entity_info() { $return = array( 'taxonomy_term' => array( 'label' => t('Taxonomy term'), + 'loader class' => 'TaxonomyTermLoader', + 'base table' => 'taxonomy_term_data', + 'fieldable' => TRUE, 'object keys' => array( 'id' => 'tid', 'bundle' => 'vocabulary_machine_name', @@ -35,8 +38,8 @@ function taxonomy_fieldable_info() { 'bundles' => array(), ), ); - foreach (taxonomy_get_vocabularies() as $vocabulary) { - $return['taxonomy_term']['bundles'][$vocabulary->machine_name] = array( + foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) { + $return['taxonomy_term']['bundles'][$machine_name] = array( 'label' => $vocabulary->name, 'admin' => array( 'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary', @@ -46,6 +49,16 @@ function taxonomy_fieldable_info() { ), ); } + $return['taxonomy_vocabulary'] = array( + 'label' => t('Taxonomy vocabulary'), + 'loader class' => 'TaxonomyVocabularyLoader', + 'base table' => 'taxonomy_vocabulary', + 'object keys' => array( + 'id' => 'vid', + ), + 'fieldable' => FALSE, + ); + return $return; } @@ -327,7 +340,7 @@ function taxonomy_vocabulary_save($vocab } cache_clear_all(); - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + entity_get_loader('taxonomy_vocabulary')->resetCache(); return $status; } @@ -358,7 +371,7 @@ function taxonomy_vocabulary_delete($vid module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary); cache_clear_all(); - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + entity_get_loader('taxonomy_vocabulary')->resetCache(); return SAVED_DELETED; } @@ -554,7 +567,7 @@ function taxonomy_terms_static_reset() { drupal_static_reset('taxonomy_term_count_nodes'); drupal_static_reset('taxonomy_get_tree'); drupal_static_reset('taxonomy_get_synonym_root'); - drupal_static_reset('taxonomy_term_load_multiple'); + entity_get_loader('taxonomy_term')->resetCache(); } /** @@ -618,21 +631,17 @@ function taxonomy_form_all($free_tags = */ function taxonomy_get_vocabularies($type = NULL) { $conditions = !empty($type) ? array('type' => $type) : NULL; - return taxonomy_vocabulary_load_multiple(array(), $conditions); + return taxonomy_vocabulary_load_multiple(FALSE, $conditions); } /** * Get names for all taxonomy vocabularies. * * @return - * An array of vocabulary names in the format 'machine_name' => 'name'. + * An array of vocabulary ids, names and machine names, keyed by machine name. */ function taxonomy_vocabulary_get_names() { - $names = array(); - $vocabularies = taxonomy_get_vocabularies(); - foreach ($vocabularies as $vocabulary) { - $names[$vocabulary->machine_name] = $vocabulary->name; - } + $names = db_query('SELECT name, machine_name, vid FROM {taxonomy_vocabulary}')->fetchAllAssoc('machine_name'); return $names; } @@ -1176,99 +1185,97 @@ function taxonomy_get_term_by_name($name } /** - * Load multiple taxonomy vocabularies based on certain conditions. - * - * This function should be used whenever you need to load more than one - * vocabulary from the database. Terms are loaded into memory and will not - * require database access if loaded again during the same page request. - * - * @param $vids - * An array of taxonomy vocabulary IDs. - * @param $conditions - * An array of conditions to add to the query. + * Return array of tids and join operator. * - * @return - * An array of vocabulary objects, indexed by vid. + * This is a wrapper function for taxonomy_terms_parse_string which is called + * by the menu system when loading a path with taxonomy terms. */ -function taxonomy_vocabulary_load_multiple($vids = array(), $conditions = array()) { - $vocabulary_cache = &drupal_static(__FUNCTION__, array()); - // Node type associations are not stored in the vocabulary table, so remove - // this from conditions into it's own variable. - if (isset($conditions['type'])) { - $type = $conditions['type']; - unset($conditions['type']); - } - - $vocabularies = array(); - - // Create a new variable which is either a prepared version of the $vids - // array for later comparison with the term cache, or FALSE if no $vids were - // passed. The $vids array is reduced as items are loaded from cache, and we - // need to know if it's empty for this reason to avoid querying the database - // when all requested items are loaded from cache. - $passed_vids = !empty($vids) ? array_flip($vids) : FALSE; +function taxonomy_terms_load($str_tids) { + $terms = taxonomy_terms_parse_string($str_tids); + return $terms; +} - // Load any available items from the internal cache. - if ($vocabulary_cache) { - if ($vids) { - $vocabularies += array_intersect_key($vocabulary_cache, $passed_vids); - // If any items were loaded, remove them from the $vids still to load. - $vids = array_keys(array_diff_key($passed_vids, $vocabularies)); +/** + * Loader class for taxonomy terms. + * + * This extends the DrupalDefaultEntityLoader class. Only alteration is that + * we match the condition on term name case-independently. + */ +class TaxonomyTermLoader extends DrupalDefaultEntityLoader { + protected $type; + public function loadMultiple($ids = array(), $conditions = array()) { + if (isset($conditions['type'])) { + $this->type = $conditions['type']; + unset($conditions['type']); + } + return parent::loadMultiple($ids, $conditions); + } + + protected function buildQuery() { + parent::buildQuery(); + // When name is passed as a condition use LIKE. + if (isset($this->conditions['name'])) { + $conditions = &$this->query->conditions(); + foreach ($conditions as $key => $condition) { + if ($condition['field'] == 'base.name') { + $conditions[$key]['operator'] = 'LIKE'; + } + } } - // If only conditions is passed, load all items from the cache. Items - // which don't match conditions will be removed later. - elseif ($conditions) { - $vocabularies = $vocabulary_cache; + // Add the machine name field from the {taxonomy_vocabulary} table. + $this->query->innerJoin('taxonomy_vocabulary', 'v', 'base.vid = v.vid'); + $this->query->addField('v', 'machine_name', 'vocabulary_machine_name'); + + if (!empty($this->type)) { + $this->query->innerJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid AND n.type = :type', array(':type' => $this->type)); } } - // Remove any loaded terms from the array if they don't match $conditions. - if ($conditions || isset($type)) { - foreach ($vocabularies as $vocabulary) { - $vocabulary_values = (array) $vocabulary; - if (array_diff_assoc($conditions, $vocabulary_values)) { - unset($vocabularies[$vocabulary->vid]); - } - if (isset($type) && !in_array($type, $vocabulary->nodes)) { - unset($vocabularies[$vocabulary->vid]); + protected function cacheGet($ids) { + $terms = parent::cacheGet($ids); + // Name matching is case insensitive, note that with some collations + // LOWER() and drupal_strtolower() may return different results. + foreach ($terms as $term) { + $term_values = (array) $term; + if (isset($this->conditions['name']) && drupal_strtolower($this->conditions['name'] != drupal_strtolower($term_values['name']))) { + unset($terms[$term->tid]); } } + return $terms; } +} - // Load any remaining vocabularies from the database, this is necessary if - // we have $vids still to load, or if no $vids were passed. - if ($vids || !$passed_vids) { - $query = db_select('taxonomy_vocabulary', 'v'); - $query->addField('n', 'type'); - $query - ->fields('v') - ->orderBy('v.weight') - ->orderBy('v.name') - ->addTag('vocabulary_access'); - - if (!empty($type)) { - $query->join('taxonomy_vocabulary_node_type', 'n', 'v.vid = n.vid AND n.type = :type', array(':type' => $type)); - } - else { - $query->leftJoin('taxonomy_vocabulary_node_type', 'n', 'v.vid = n.vid'); +/** + * Loader class for taxonomy vocabularies. + * + * This extends the DrupalDefaultEntityLoader class, adding required special + * handling for taxonomy vocabulary objects. + */ +class TaxonomyVocabularyLoader extends DrupalDefaultEntityLoader { + protected $type; + public function loadMultiple($ids = array(), $conditions = array()) { + if (isset($conditions['type'])) { + $this->type = $conditions['type']; + unset($conditions['type']); } + return parent::loadMultiple($ids, $conditions); + } - // If the $vids array is populated, add those to the query. - if ($vids) { - $query->condition('v.vid', $vids, 'IN'); + protected function buildQuery() { + parent::buildQuery(); + if (!empty($this->type)) { + $this->query->innerJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid AND n.type = :type', array(':type' => $this->type)); } - - // If the conditions array is populated, add those to the query. - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('v.' . $field, $value); - } + else { + $this->query->leftJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid'); } - $result = $query->execute(); + $this->query->addField('n', 'type'); + $this->query->orderBy('base.weight'); + $this->query->orderBy('base.name'); + } - $queried_vocabularies = array(); - $node_types = array(); - foreach ($result as $record) { + protected function attachLoad(&$records) { + foreach ($records as $record) { // If no node types are associated with a vocabulary, the LEFT JOIN will // return a NULL value for type. if (isset($record->type)) { @@ -1281,45 +1288,9 @@ function taxonomy_vocabulary_load_multip } $queried_vocabularies[$record->vid] = $record; } - - // Invoke hook_taxonomy_vocabulary_load() on the vocabularies loaded from - // the database and add them to the static cache. - if (!empty($queried_vocabularies)) { - foreach (module_implements('taxonomy_vocabulary_load') as $module) { - $function = $module . '_taxonomy_vocabulary_load'; - $function($queried_vocabularies); - } - $vocabularies += $queried_vocabularies; - $vocabulary_cache += $queried_vocabularies; - } + $records = $queried_vocabularies; + parent::attachLoad($records); } - - // Ensure that the returned array is ordered the same as the original $vids - // array if this was passed in and remove any invalid vids. - if ($passed_vids) { - // Remove any invalid vids from the array. - $passed_vids = array_intersect_key($passed_vids, $vocabularies); - foreach ($vocabularies as $vocabulary) { - $passed_vids[$vocabulary->vid] = $vocabulary; - } - $vocabularies = $passed_vids; - } - - return $vocabularies; -} - -/** - * Return the vocabulary object matching a vocabulary ID. - * - * @param $vid - * The vocabulary's ID. - * - * @return - * The vocabulary object with all of its metadata, if exists, FALSE otherwise. - * Results are statically cached. - */ -function taxonomy_vocabulary_load($vid) { - return reset(taxonomy_vocabulary_load_multiple(array($vid), array())); } /** @@ -1329,6 +1300,8 @@ function taxonomy_vocabulary_load($vid) * from the database. Terms are loaded into memory and will not require * database access if loaded again during the same page request. * + * @see entity_load_multiple() + * * @param $tids * An array of taxonomy term IDs. * @param $conditions @@ -1338,115 +1311,42 @@ function taxonomy_vocabulary_load($vid) * An array of term objects, indexed by tid. */ function taxonomy_term_load_multiple($tids = array(), $conditions = array()) { - $term_cache = &drupal_static(__FUNCTION__, array()); - - // Node type associations are not stored in the taxonomy_term_data table, so - // remove this from conditions into it's own variable. - if (isset($conditions['type'])) { - $type = $conditions['type']; - unset($conditions['type']); - } - - $terms = array(); - - // Create a new variable which is either a prepared version of the $tids - // array for later comparison with the term cache, or FALSE if no $tids were - // passed. The $tids array is reduced as items are loaded from cache, and we - // need to know if it's empty for this reason to avoid querying the database - // when all requested terms are loaded from cache. - $passed_tids = !empty($tids) ? array_flip($tids) : FALSE; - - // Load any available terms from the internal cache. - if ($term_cache) { - if ($tids) { - $terms += array_intersect_key($term_cache, $passed_tids); - // If any terms were loaded, remove them from the $tids still to load. - $tids = array_keys(array_diff_key($passed_tids, $terms)); - } - // If only conditions is passed, load all terms from the cache. Terms - // which don't match conditions will be removed later. - elseif ($conditions) { - $terms = $term_cache; - } - } - - // Remove any loaded terms from the array if they don't match $conditions. - if ($conditions) { - // Name matching is case insensitive, note that with some collations - // LOWER() and drupal_strtolower() may return different results. - foreach ($terms as $term) { - $term_values = (array) $term; - if (isset($conditions['name']) && drupal_strtolower($conditions['name'] != drupal_strtolower($term_values['name']))) { - unset($terms[$term->tid]); - } - elseif (array_diff_assoc($conditions, $term_values)) { - unset($terms[$term->tid]); - } - } - } - - // Load any remaining terms from the database, this is necessary if we have - // $tids still to load, or if $conditions was passed without $tids. - if ($tids || ($conditions && !$passed_tids)) { - $query = db_select('taxonomy_term_data', 't'); - $query->addTag('term_access'); - $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid'); - $taxonomy_term_data = drupal_schema_fields_sql('taxonomy_term_data'); - $query->addField('v', 'machine_name', 'vocabulary_machine_name'); - $query - ->fields('t', $taxonomy_term_data) - ->addTag('term_access'); - - // If the $tids array is populated, add those to the query. - if ($tids) { - $query->condition('t.tid', $tids, 'IN'); - } - - if (!empty($type)) { - $query->join('taxonomy_vocabulary_node_type', 'n', 't.vid = n.vid AND n.type = :type', array(':type' => $type)); - } - - // If the conditions array is populated, add those to the query. - if ($conditions) { - // When name is passed as a condition use LIKE. - if (isset($conditions['name'])) { - $query->condition('t.name', $conditions['name'], 'LIKE'); - unset($conditions['name']); - } - foreach ($conditions as $field => $value) { - $query->condition('t.' . $field, $value); - } - } - $queried_terms = $query->execute()->fetchAllAssoc('tid'); - - if (!empty($queried_terms)) { - - // Attach fields. - field_attach_load('taxonomy_term', $queried_terms); - - // Invoke hook_taxonomy_term_load() and add the term objects to the - // static cache. - foreach (module_implements('taxonomy_term_load') as $module) { - $function = $module . '_taxonomy_term_load'; - $function($queried_terms); - } - $terms += $queried_terms; - $term_cache += $queried_terms; - } - } + return entity_load_multiple('taxonomy_term', $tids, $conditions); +} - // Ensure that the returned array is ordered the same as the original $tids - // array if this was passed in and remove any invalid tids. - if ($passed_tids) { - // Remove any invalid tids from the array. - $passed_tids = array_intersect_key($passed_tids, $terms); - foreach ($terms as $term) { - $passed_tids[$term->tid] = $term; - } - $terms = $passed_tids; - } +/** + * Load multiple taxonomy vocabularies based on certain conditions. + * + * This function should be used whenever you need to load more than one + * vocabulary from the database. Terms are loaded into memory and will not + * require database access if loaded again during the same page request. + * + * @see entity_load_multiple() + * + * @param $vids + * An array of taxonomy vocabulary IDs, or FALSE to load all vocabularies. + * @param $conditions + * An array of conditions to add to the query. + * + * @return + * An array of vocabulary objects, indexed by vid. + */ +function taxonomy_vocabulary_load_multiple($vids = array(), $conditions = array()) { + return entity_load_multiple('taxonomy_vocabulary', $vids, $conditions); +} - return $terms; +/** + * Return the vocabulary object matching a vocabulary ID. + * + * @param $vid + * The vocabulary's ID. + * + * @return + * The vocabulary object with all of its metadata, if exists, FALSE otherwise. + * Results are statically cached. + */ +function taxonomy_vocabulary_load($vid) { + return reset(taxonomy_vocabulary_load_multiple(array($vid))); } /** Index: modules/taxonomy/taxonomy.test =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v retrieving revision 1.43 diff -u -p -r1.43 taxonomy.test --- modules/taxonomy/taxonomy.test 4 Aug 2009 06:50:07 -0000 1.43 +++ modules/taxonomy/taxonomy.test 9 Aug 2009 14:08:38 -0000 @@ -162,7 +162,7 @@ class TaxonomyVocabularyFunctionalTest e // Check the created vocabulary. $vocabularies = taxonomy_get_vocabularies(); $vid = $vocabularies[count($vocabularies)-1]->vid; - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + entity_get_loader('taxonomy_vocabulary')->resetCache(); $vocabulary = taxonomy_vocabulary_load($vid); $this->assertTrue($vocabulary, t('Vocabulary found in database')); @@ -175,7 +175,7 @@ class TaxonomyVocabularyFunctionalTest e // Confirm deletion. $this->drupalPost(NULL, NULL, t('Delete')); $this->assertRaw(t('Deleted vocabulary %name.', array('%name' => $vocabulary->name)), t('Vocabulary deleted')); - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + entity_get_loader('taxonomy_vocabulary')->resetCache(); $this->assertFalse(taxonomy_vocabulary_load($vid), t('Vocabulary is not found in the database')); } } @@ -271,8 +271,7 @@ class TaxonomyVocabularyUnitTest extends // Fetch the names for all vocabularies, confirm that they are keyed by // machine name. $names = taxonomy_vocabulary_get_names(); - $this->assertTrue(in_array($vocabulary1->name, $names), t('Vocabulary 1 name found.')); - $this->assertTrue(isset($names[$vocabulary1->machine_name]), t('Vocabulary names are keyed by machine name.')); + $this->assertEqual($names[$vocabulary1->machine_name]->name, $vocabulary1->name, t('Vocabulary 1 name found.')); // Fetch all of the vocabularies using taxonomy_get_vocabularies(). // Confirm that the vocabularies are ordered by weight. @@ -295,7 +294,7 @@ class TaxonomyVocabularyUnitTest extends $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name and ID.')); // Fetch vocabulary 1 with specified node type. - drupal_static_reset('taxonomy_vocabulary_load_multiple'); + entity_get_loader('taxonomy_vocabulary')->resetCache(); $vocabulary_node_type = current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('type' => 'article'))); $this->assertEqual($vocabulary_node_type, $vocabulary1, t('Vocabulary with specified node type loaded successfully.')); } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.1019 diff -u -p -r1.1019 user.module --- modules/user/user.module 8 Aug 2009 20:52:33 -0000 1.1019 +++ modules/user/user.module 9 Aug 2009 14:08:41 -0000 @@ -84,12 +84,15 @@ function user_theme() { } /** - * Implement hook_fieldable_info(). + * Implement hook_entity_info(). */ -function user_fieldable_info() { +function user_entity_info() { $return = array( 'user' => array( 'label' => t('User'), + 'loader class' => 'UserLoader', + 'base table' => 'users', + 'fieldable' => TRUE, 'object keys' => array( 'id' => 'uid', ), @@ -149,68 +152,29 @@ function user_external_load($authname) { * @return * An array of user objects, indexed by uid. * + * @see entity_load_multiple() * @see user_load() * @see user_load_by_mail() * @see user_load_by_name() */ function user_load_multiple($uids = array(), $conditions = array(), $reset = FALSE) { - static $user_cache = array(); - if ($reset) { - $user_cache = array(); - } - - $users = array(); - - // Create a new variable which is either a prepared version of the $uids - // array for later comparison with the user cache, or FALSE if no $uids were - // passed. The $uids array is reduced as items are loaded from cache, and we - // need to know if it's empty for this reason to avoid querying the database - // when all requested users are loaded from cache. - $passed_uids = !empty($uids) ? array_flip($uids) : FALSE; - - // Load any available users from the internal cache. - if ($user_cache) { - if ($uids && !$conditions) { - $users += array_intersect_key($user_cache, $passed_uids); - // If any users were loaded, remove them from the $uids still to load. - $uids = array_keys(array_diff_key($passed_uids, $users)); - } - } - - // Load any remaining users from the database, this is necessary if we have - // $uids still to load, or if $conditions was passed without $uids. - if ($uids || ($conditions && !$passed_uids)) { - $query = db_select('users', 'u')->fields('u'); - - // If the $uids array is populated, add those to the query. - if ($uids) { - $query->condition('u.uid', $uids, 'IN'); - } - // If the conditions array is populated, add those to the query. - if ($conditions) { - // TODO D7: Using LIKE() to get a case insensitive comparison because Crell - // and chx promise that dbtng will map it to ILIKE in postgres. - if (isset($conditions['name'])) { - $query->condition('u.name', $conditions['name'], 'LIKE'); - unset($conditions['name']); - } - if (isset($conditions['mail'])) { - $query->condition('u.mail', $conditions['mail'], 'LIKE'); - unset($conditions['mail']); - } - foreach ($conditions as $field => $value) { - $query->condition('u.' . $field, $value); - } - } - $result = $query->execute(); + return entity_load_multiple('user', $uids, $conditions, $reset); +} - $queried_users = array(); +/** + * Loader class for users. + * + * This extends the DrupalDefaultEntityLoader class, adding required special + * handling for user objects. + */ +class UserLoader extends DrupalDefaultEntityLoader { + function attachLoad(&$queried_users) { // Build an array of user picture IDs so that these can be fetched later. $picture_fids = array(); - foreach ($result as $record) { + foreach ($queried_users as $key => $record) { $picture_fids[] = $record->picture; - $queried_users[$record->uid] = drupal_unpack($record); - $queried_users[$record->uid]->roles = array(); + $queried_users[$key] = drupal_unpack($record); + $queried_users[$key]->roles = array(); if ($record->uid) { $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; } @@ -219,57 +183,29 @@ function user_load_multiple($uids = arra } } - if (!empty($queried_users)) { - // Add any additional roles from the database. - $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users))); - foreach ($result as $record) { - $queried_users[$record->uid]->roles[$record->rid] = $record->name; - } + // Add any additional roles from the database. + $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users))); + foreach ($result as $record) { + $queried_users[$record->uid]->roles[$record->rid] = $record->name; + } - // Add the full file objects for user pictures if enabled. - if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) { - $pictures = file_load_multiple($picture_fids); - foreach ($queried_users as $account) { - if (!empty($account->picture) && isset($pictures[$account->picture])) { - $account->picture = $pictures[$account->picture]; - } - else { - $account->picture = NULL; - } + // Add the full file objects for user pictures if enabled. + if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) { + $pictures = file_load_multiple($picture_fids); + foreach ($queried_users as $account) { + if (!empty($account->picture) && isset($pictures[$account->picture])) { + $account->picture = $pictures[$account->picture]; + } + else { + $account->picture = NULL; } } - - field_attach_load('user', $queried_users); - - // Invoke hook_user_load() on the users loaded from the database - // and add them to the static cache. - foreach (module_implements('user_load') as $module) { - $function = $module . '_user_load'; - $function($queried_users); - } - - - - $users = $users + $queried_users; - $user_cache = $user_cache + $queried_users; } + // Call the default attachLoad() method. This will add fields and call hook_user_load(). + parent::attachLoad($queried_users); } - - // Ensure that the returned array is ordered the same as the original $uids - // array if this was passed in and remove any invalid uids. - if ($passed_uids) { - // Remove any invalid uids from the array. - $passed_uids = array_intersect_key($passed_uids, $users); - foreach ($users as $user) { - $passed_uids[$user->uid] = $user; - } - $users = $passed_uids; - } - - return $users; } - /** * Fetch a user object. *