From 3f36af04de485a0afa575c17a5d622545e183c75 Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Tue, 25 Aug 2009 21:53:48 +0000
Subject: [PATCH] - Patch #460320 by catch, fago, Frando: standardized,
 pluggable entity loading for nodes, users, taxonomies, files and comments.

---
 includes/common.inc                        |  89 +++++
 includes/entity.inc                        | 294 +++++++++++++++
 includes/file.inc                          |  29 +-
 install.php                                |   3 +
 modules/comment/comment.module             | 127 ++++---
 modules/field/field.api.php                |  94 -----
 modules/field/field.info.inc               |  47 +--
 modules/field/field.test                   |   8 +-
 modules/forum/forum.test                   |   2 +-
 modules/node/node.module                   | 187 ++--------
 modules/simpletest/tests/field_test.module |   4 +-
 modules/system/system.admin.inc            |   1 +
 modules/system/system.api.php              | 106 ++++++
 modules/system/system.module               |  16 +
 modules/taxonomy/taxonomy.module           | 395 +++++++--------------
 modules/taxonomy/taxonomy.test             |   9 +-
 modules/user/user.module                   | 135 ++-----
 update.php                                 |   3 +-
 18 files changed, 822 insertions(+), 727 deletions(-)
 create mode 100644 includes/entity.inc

diff --git a/includes/common.inc b/includes/common.inc
index cabcf7dfa059..bb4777dccde5 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -5075,6 +5075,95 @@ function drupal_check_incompatibility($v, $current_version) {
   }
 }
 
+/**
+ * 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 entity_get_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,
+          'controller class' => 'DrupalDefaultEntityController',
+          'static cache' => TRUE,
+          'load hook' => $name . '_load',
+        );
+      }
+      // 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
+ * DrupalEntityController interface. By default, DrupalDefaultEntityController
+ * is used. Entity types can specify that a different class should be used by
+ * setting the 'controller class' key in hook_entity_info(). These classes can
+ * either implement the DrupalEntityController interface, or, most commonly,
+ * extend the DrupalDefaultEntityController class. See node_entity_info() and
+ * the NodeController in node.module as an example.
+ *
+ * @see hook_entity_info()
+ * @see DrupalEntityController
+ * @see DrupalDefaultEntityController
+ *
+ * @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 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($entity_type, $ids = array(), $conditions = array(), $reset = FALSE) {
+  if ($reset) {
+    entity_get_controller($entity_type)->resetCache();
+  }
+  return entity_get_controller($entity_type)->load($ids, $conditions);
+}
+
+/**
+ * Get the entity controller class for an entity type.
+ */
+function entity_get_controller($entity_type) {
+  $controllers = &drupal_static(__FUNCTION__, array());
+  if (!isset($controllers[$entity_type])) {
+    $type_info = entity_get_info($entity_type);
+    $class = $type_info['controller class'];
+    $controllers[$entity_type] = new $class($entity_type);
+  }
+  return $controllers[$entity_type];
+}
+
 /**
  * Performs one or more XML-RPC request(s).
  *
diff --git a/includes/entity.inc b/includes/entity.inc
new file mode 100644
index 000000000000..e50cf6fb3fe6
--- /dev/null
+++ b/includes/entity.inc
@@ -0,0 +1,294 @@
+<?php
+// $Id$
+
+/**
+ * Interface for entity controller classes.
+ *
+ * All entity controller classes specified via the 'controller class' key
+ * returned by hook_entity_info() or hook_entity_info_alter() have to implement
+ * this interface.
+ *
+ * Most simple, SQL-based entity controllers will do better by extending
+ * DrupalDefaultEntityController instead of implementing this interface
+ * directly.
+ */
+interface DrupalEntityControllerInterface {
+  /**
+   * Constructor.
+   *
+   * @param $entityType
+   *   The entity type for which the instance is created.
+   */
+  public function __construct($entityType);
+
+  /**
+   * Reset the internal, static entity cache.
+   */
+  public function resetCache();
+
+  /**
+   * Load one or more entities.
+   *
+   * @param $ids
+   *   An array of entity IDs, or FALSE to load all entities.
+   * @param $conditions
+   *   An array of conditions in the form 'field' => $value.
+   *
+   * @return
+   *   An array of entity objects indexed by their ids.
+   */
+  public function load($ids = array(), $conditions = array());
+}
+
+/**
+ * Default implementation of DrupalEntityControllerInterface.
+ *
+ * This class can be used as-is by most simple entity types. Entity types
+ * requiring special handling can extend the class.
+ */
+class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
+
+  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 = entity_get_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 load($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, for example hook_node_load().
+    if (!empty($queried_entities)) {
+      $this->attachLoad($queried_entities);
+      $entities += $queried_entities;
+    }
+
+    if ($this->cache) {
+      // Add entities to the cache if we are 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 extended, and the default query can be constructed by
+   * calling parent::buildQuery(). This is usually necessary when the object
+   * being loaded needs to be augmented with additional data from another
+   * table, such as loading node type into comments or vocabulary machine name
+   * into terms, however it can also support $conditions on different tables.
+   * See NodeController::buildQuery() or TaxonomyTermController::buildQuery()
+   * for examples.
+   */
+  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_map_assoc(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, and revision uid to
+      // revision_uid before adding them to the query.
+      // TODO: This is node specific and has to be moved into NodeController.
+      unset($entity_revision_fields['timestamp']);
+      $this->query->addField('revision', 'timestamp', 'revision_timestamp');
+      unset($entity_revision_fields['uid']);
+      $this->query->addField('revision', 'uid', 'revision_uid');
+
+      // 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_TYPE_load() on the loaded entities. For example
+   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+   * expects special parameters apart from the queried entities, you can set
+   * $this->hookLoadArguments prior to calling the method.
+   * See NodeController::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_TYPE_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->entityInfo['load hook']) as $module) {
+      call_user_func_array($module . '_' . $this->entityInfo['load hook'], $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;
+  }
+}
diff --git a/includes/file.inc b/includes/file.inc
index 8047033bee84..aa9f425e9f6a 100644
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -432,34 +432,7 @@ function file_create_htaccess($directory, $private = TRUE) {
  * @see file_load()
  */
 function file_load_multiple($fids = array(), $conditions = array()) {
-  // If they don't provide any criteria return nothing rather than all files.
-  if (!$fids && !$conditions) {
-    return array();
-  }
-  $query = db_select('file', '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('file', $fids, $conditions);
 }
 
 /**
diff --git a/install.php b/install.php
index f5719a1cd2a5..f57385d6b576 100644
--- a/install.php
+++ b/install.php
@@ -248,11 +248,14 @@ function install_begin_request(&$install_state) {
   // 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['user']['filename'] = 'modules/user/user.module';
   module_list(TRUE, FALSE, FALSE, $module_list);
   drupal_load('module', 'system');
   drupal_load('module', 'filter');
+  drupal_load('module', 'user');
 
   // Prepare for themed output, if necessary. We need to run this at the
   // beginning of the page request to avoid a different theme accidentally
diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index 8967731c7a83..d63fd3c29119 100644
--- a/modules/comment/comment.module
+++ b/modules/comment/comment.module
@@ -95,6 +95,37 @@ function comment_help($path, $arg) {
   }
 }
 
+/**
+ * Implement hook_entity_info() {
+ */
+function comment_entity_info() {
+  $return =  array(
+    'comment' => array(
+      'label' => t('Comment'),
+      'base table' => 'comment',
+      'fieldable' => TRUE,
+      'controller class' => 'CommentController',
+      'object keys' => array(
+        'id' => 'cid',
+        'bundle' => 'node_type',
+      ),
+      'bundle keys' => array(
+        'bundle' => 'type',
+      ),
+      'bundles' => array(),
+      'static cache' => FALSE,
+    ),
+  );
+
+  foreach (node_type_get_names() as $type => $name) {
+    $return['comment']['bundles']['comment_node_' . $type] = array(
+      'label' => $name,
+    );
+  }
+
+  return $return;
+}
+
 /**
  * Implement hook_theme().
  */
@@ -190,31 +221,6 @@ function comment_menu() {
   return $items;
 }
 
-/**
- * Implement hook_fieldable_info().
- */
-function comment_fieldable_info() {
-  $return = array(
-    'comment' => array(
-      'label' => t('Comment'),
-      'object keys' => array(
-        'id' => 'cid',
-        'bundle' => 'node_type',
-      ),
-      'bundle keys' => array(
-        'bundle' => 'type',
-      ),
-      'bundles' => array(),
-    ),
-  );
-  foreach (node_type_get_names() as $type => $name) {
-    $return['comment']['bundles']['comment_node_' . $type] = array(
-      'label' => $name,
-    );
-  }
-  return $return;
-}
-
 /**
  * Implement hook_node_type_insert().
  */
@@ -1437,47 +1443,7 @@ function comment_operations($action = NULL) {
  *  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;
+  return entity_load('comment', $cids, $conditions);
 }
 
 /**
@@ -1493,6 +1459,35 @@ function comment_load($cid) {
   return $comment ? $comment[$cid] : FALSE;;
 }
 
+/**
+ * Controller class for comments.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for comment objects.
+ */
+class CommentController extends DrupalDefaultEntityController {
+  protected function buildQuery() {
+    parent::buildQuery();
+    // Specify additional fields from the user and node tables.
+    $this->query->innerJoin('node', 'n', 'base.nid = n.nid');
+    $this->query->addField('n', 'type', 'node_type');
+    $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);
+      $comment->node_type = 'comment_node_' . $comment->node_type;
+      $comments[$key] = $comment;
+    }
+  }
+}
+
 /**
  * Get replies count for a comment.
  *
diff --git a/modules/field/field.api.php b/modules/field/field.api.php
index 64174560b0f3..d6f29609d747 100644
--- a/modules/field/field.api.php
+++ b/modules/field/field.api.php
@@ -6,100 +6,6 @@
  * @{
  */
 
-/**
- * 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;
-}
-
 /**
  * Expose "pseudo-field" components on fieldable objects.
  *
diff --git a/modules/field/field.info.inc b/modules/field/field.info.inc
index 3c9c1df3e519..0d895fa88110 100644
--- a/modules/field/field.info.inc
+++ b/modules/field/field.info.inc
@@ -54,10 +54,11 @@ function _field_info_cache_clear() {
  *   * 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 entity_get_info().
  */
 function _field_info_collate_types($reset = FALSE) {
   static $info;
@@ -124,27 +125,29 @@ function _field_info_collate_types($reset = FALSE) {
       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 ($fieldable_types as $name => $fieldable_info) {
-          // Provide defaults.
-          $fieldable_info += array(
-            'cacheable' => TRUE,
-            'translation_handlers' => array(),
-            '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.
-          if (empty($fieldable_info['object keys']['bundle']) && empty($fieldable_info['bundles'])) {
-            $fieldable_info['bundles'] = array($name => array('label' => $fieldable_info['label']));
+      foreach (module_implements('entity_info') as $module) {
+        $entities = (array) module_invoke($module, 'entity_info');
+        foreach ($entities as $name => $entity_info) {
+          if (!empty($entity_info['fieldable'])) {
+            // Provide defaults.
+            $entity_info += array(
+              'cacheable' => TRUE,
+              'translation_handlers' => array(),
+              'bundles' => array(),
+            );
+            $entity_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.
+            if (empty($entity_info['object keys']['bundle']) && empty($entity_info['bundles'])) {
+              $entity_info['bundles'] = array($name => array('label' => $entity_info['label']));
+            }
+            $info['fieldable types'][$name] = $entity_info;
+            $info['fieldable types'][$name]['module'] = $module;
           }
-          $info['fieldable types'][$name] = $fieldable_info;
-          $info['fieldable types'][$name]['module'] = $module;
         }
       }
       drupal_alter('fieldable_info', $info['fieldable types']);
diff --git a/modules/field/field.test b/modules/field/field.test
index db498ae6ef62..ab34ed4f57b3 100644
--- a/modules/field/field.test
+++ b/modules/field/field.test
@@ -600,7 +600,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
 
   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);
 
@@ -622,7 +622,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
     $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Data is 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);
 
@@ -638,7 +638,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
 
   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);
 
@@ -679,7 +679,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
     $this->assertEqual(count($entity->{$field_name}[$langcode]), 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
diff --git a/modules/forum/forum.test b/modules/forum/forum.test
index 937d8e2dbe81..ee217e5fb319 100644
--- a/modules/forum/forum.test
+++ b/modules/forum/forum.test
@@ -136,7 +136,7 @@ class ForumTestCase extends DrupalWebTestCase {
     $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');
+    entity_get_controller('taxonomy_vocabulary')->resetCache();
     $current_settings = taxonomy_vocabulary_load($vid);
 
     // Make sure we actually edited the vocabulary properly.
diff --git a/modules/node/node.module b/modules/node/node.module
index fa5cfc832159..1218b9d32356 100644
--- a/modules/node/node.module
+++ b/modules/node/node.module
@@ -147,12 +147,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'),
+      'controller class' => 'NodeController',
+      'base table' => 'node',
+      'revision table' => 'node_revision',
+      'fieldable' => TRUE,
       'object keys' => array(
         'id' => 'nid',
         'revision' => 'vid',
@@ -224,7 +228,7 @@ function node_field_extra_fields($bundle) {
  * 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 <code>node_comment_statistics</code> table so that the
  *   <code>comment_count</code> field is available, a title attribute will
  *   be added to show the number of comments.
@@ -720,12 +724,14 @@ function node_invoke($node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
 }
 
 /**
- * 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()
+ *
  * @param $nids
  *   An array of node IDs.
  * @param $conditions
@@ -737,150 +743,7 @@ function node_invoke($node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
  *   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');
-
-    // The columns vid, title, status, comment, promote, moderate, and sticky
-    // are all provided by node_revision, so remove them.
-    $node_fields = array_diff($node_fields, array('vid', 'title', 'status', 'comment', 'promote', 'moderate', 'sticky'));
-    $query->fields('n', $node_fields);
-
-    // Add all fields from the {node_revision} table.
-    $node_revision_fields = drupal_schema_fields_sql('node_revision');
-
-    // {node_revision}.nid is provided by node, and {node_revision}.uid and
-    // {node_revision}.timestamp will be added with aliases, so remove them
-    // before adding to the query.
-    $node_revision_fields = array_diff($node_revision_fields, array('nid', 'uid', 'timestamp'));
-    $query->fields('r', $node_revision_fields);
-
-    // Add {node_revision}.uid with alias revision_uid to avoid the name
-    // collision with {node}.uid, otherwise the revision author would be loaded
-    // as $node->uid.
-    $query->addField('r', 'uid', 'revision_uid');
-
-    // Add {node_revision}.timestamp with alias revision_timestamp for clarity.
-    $query->addField('r', 'timestamp', 'revision_timestamp');
-
-    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('node', $nids, $conditions, $reset);
 }
 
 /**
@@ -899,7 +762,6 @@ function node_load_multiple($nids = array(), $conditions = array(), $reset = FAL
 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;
 }
 
@@ -3262,3 +3124,30 @@ function node_requirements($phase) {
   );
   return $requirements;
 }
+
+/**
+ * Controller class for nodes.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for node objects.
+ */
+class NodeController extends DrupalDefaultEntityController {
+  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($node_type) . '_load';
+        $function($nodes_of_type);
+      }
+    }
+    $this->hookLoadArguments[] = array_keys($typed_nodes);
+    parent::attachLoad($nodes);
+  }
+}
diff --git a/modules/simpletest/tests/field_test.module b/modules/simpletest/tests/field_test.module
index 54ae3d405487..fce1e4735045 100644
--- a/modules/simpletest/tests/field_test.module
+++ b/modules/simpletest/tests/field_test.module
@@ -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(
@@ -81,6 +82,7 @@ function field_test_fieldable_info() {
         'revision' => 'ftvid',
         'bundle' => 'fttype',
       ),
+      'fieldable' => TRUE,
       'cacheable' => TRUE,
       'bundles' => $bundles,
     ),
diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc
index ac19b3a05b3e..3b115c872237 100644
--- a/modules/system/system.admin.inc
+++ b/modules/system/system.admin.inc
@@ -1001,6 +1001,7 @@ function system_modules_submit($form, &$form_state) {
   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();
 
diff --git a/modules/system/system.api.php b/modules/system/system.api.php
index 8d1bda40591a..82f7240c72a9 100644
--- a/modules/system/system.api.php
+++ b/modules/system/system.api.php
@@ -11,6 +11,112 @@
  * @{
  */
 
+/**
+ * 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() and, optionally, to which fields can be
+ * attached).
+ *
+ * @see entity_load()
+ * @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.
+ *   controller class: The name of the class that is used to load the objects.
+ *     The class has to implement the DrupalEntityController interface. Leave
+ *     blank to use the DefaultDrupalEntityController implementation.
+ *   base table: (used by DefaultDrupalEntityController) The name of the entity
+ *     type's base table.
+ *   static cache: (used by DefaultDrupalEntityController) FALSE to disable
+ *     static caching of entities during a page request. Defaults to TRUE.
+ *   load hook: The name of the hook which should be invoked by
+ *   DrupalDefaultEntityController:attachLoad(), for example 'node_load'.
+ *   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'),
+      'controller class' => 'NodeController',
+      '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 controller class for nodes to an alternate implementation of the
+  // DrupalEntityController interface.
+  $entity_info['node']['controller class'] = 'MyCustomNodeController';
+}
+
 /**
  * Perform periodic actions.
  *
diff --git a/modules/system/system.module b/modules/system/system.module
index a4c0a600dcd1..2e5101677e0e 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -266,6 +266,22 @@ function system_rdf_namespaces() {
   );
 }
 
+/**
+ * Implement hook_entity_info().
+ */
+function system_entity_info() {
+  return array(
+    'file' => array(
+      'label' => t('File'),
+      'base table' => 'file',
+      'object keys' => array(
+        'id' => 'fid',
+      ),
+      'static cache' => FALSE,
+    ),
+  );
+}
+
 /**
  * Implement hook_elements().
  */
diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module
index f8ec38c08e17..0d1fde908ec0 100644
--- a/modules/taxonomy/taxonomy.module
+++ b/modules/taxonomy/taxonomy.module
@@ -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'),
+      'controller class' => 'TaxonomyTermController',
+      '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'),
+    'controller class' => 'TaxonomyVocabularyController',
+    'base table' => 'taxonomy_vocabulary',
+    'object keys' => array(
+      'id' => 'vid',
+    ),
+    'fieldable' => FALSE,
+  );
+
   return $return;
 }
 
@@ -64,25 +77,6 @@ function taxonomy_field_build_modes($obj_type) {
   return $modes;
 }
 
-/**
- * Implement hook_field_extra_fields().
- */
-function taxonomy_field_extra_fields($bundle) {
-  $extra = array();
-
-  if ($type = node_type_get_type($bundle)) {
-    if (taxonomy_get_vocabularies($bundle)) {
-      $extra['taxonomy'] = array(
-        'label' => t('Taxonomy'),
-        'description' => t('Taxonomy module element.'),
-        'weight' => -3,
-      );
-    }
-  }
-
-  return $extra;
-}
-
 /**
  * Implement hook_theme().
  */
@@ -365,7 +359,7 @@ function taxonomy_vocabulary_save($vocabulary) {
   }
 
   cache_clear_all();
-  drupal_static_reset('taxonomy_vocabulary_load_multiple');
+  entity_get_controller('taxonomy_vocabulary')->resetCache();
 
   return $status;
 }
@@ -396,7 +390,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_controller('taxonomy_vocabulary')->resetCache();
 
   return SAVED_DELETED;
 }
@@ -592,7 +586,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_controller('taxonomy_term')->resetCache();
 }
 
 /**
@@ -656,21 +650,17 @@ function taxonomy_form_all($free_tags = 0) {
  */
 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, 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;
 }
 
@@ -1227,99 +1217,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;
-
-  // 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));
-    }
-    // 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;
+function taxonomy_terms_load($str_tids) {
+  $terms = taxonomy_terms_parse_string($str_tids);
+  return $terms;
+}
+
+/**
+ * Controller class for taxonomy terms.
+ *
+ * This extends the DrupalDefaultEntityController class. Only alteration is
+ * that we match the condition on term name case-independently.
+ */
+class TaxonomyTermController extends DrupalDefaultEntityController {
+  protected $type;
+  public function load($ids = array(), $conditions = array()) {
+    if (isset($conditions['type'])) {
+      $this->type = $conditions['type'];
+      unset($conditions['type']);
     }
+    return parent::load($ids, $conditions);
   }
 
-  // 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 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';
+        }
       }
     }
-  }
+    // 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');
 
-  // 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));
+    if (!empty($this->type)) {
+      $this->query->innerJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid AND n.type = :type', array(':type' => $this->type));
     }
-    else {
-      $query->leftJoin('taxonomy_vocabulary_node_type', 'n', 'v.vid = n.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;
+  }
+}
 
-    // If the $vids array is populated, add those to the query.
-    if ($vids) {
-      $query->condition('v.vid', $vids, 'IN');
+/**
+ * Controller class for taxonomy vocabularies.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for taxonomy vocabulary objects.
+ */
+class TaxonomyVocabularyController extends DrupalDefaultEntityController {
+  protected $type;
+  public function load($ids = array(), $conditions = array()) {
+    if (isset($conditions['type'])) {
+      $this->type = $conditions['type'];
+      unset($conditions['type']);
     }
+    return parent::load($ids, $conditions);
+  }
 
-    // If the conditions array is populated, add those to the query.
-    if ($conditions) {
-      foreach ($conditions as $field => $value) {
-        $query->condition('v.' . $field, $value);
-      }
+  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));
     }
-    $result = $query->execute();
+    else {
+      $this->query->leftJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid');
+    }
+    $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)) {
@@ -1332,45 +1320,9 @@ function taxonomy_vocabulary_load_multiple($vids = array(), $conditions = array(
       }
       $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()));
 }
 
 /**
@@ -1380,6 +1332,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()
+ *
  * @param $tids
  *  An array of taxonomy term IDs.
  * @param $conditions
@@ -1389,115 +1343,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('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()
+ *
+ * @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('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)));
 }
 
 /**
@@ -2088,7 +1969,7 @@ function _taxonomy_clean_field_cache($term) {
   // Determine object types that are not cacheable.
   $obj_types = array();
   foreach (field_info_fieldable_types() as $obj_type => $info) {
-    if (!$info['cacheable']) {
+    if (isset($info['cacheable']) && !$info['cacheable']) {
       $obj_types[] = $obj_type;
     }
   }
diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test
index 66e4b88526fc..dc3a4733d3ac 100644
--- a/modules/taxonomy/taxonomy.test
+++ b/modules/taxonomy/taxonomy.test
@@ -162,7 +162,7 @@ class TaxonomyVocabularyFunctionalTest extends TaxonomyWebTestCase {
     // Check the created vocabulary.
     $vocabularies = taxonomy_get_vocabularies();
     $vid = $vocabularies[count($vocabularies)-1]->vid;
-    drupal_static_reset('taxonomy_vocabulary_load_multiple');
+    entity_get_controller('taxonomy_vocabulary')->resetCache();
     $vocabulary = taxonomy_vocabulary_load($vid);
     $this->assertTrue($vocabulary, t('Vocabulary found in database'));
 
@@ -175,7 +175,7 @@ class TaxonomyVocabularyFunctionalTest extends TaxonomyWebTestCase {
     // 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_controller('taxonomy_vocabulary')->resetCache();
     $this->assertFalse(taxonomy_vocabulary_load($vid), t('Vocabulary is not found in the database'));
   }
 }
@@ -271,8 +271,7 @@ class TaxonomyVocabularyUnitTest extends TaxonomyWebTestCase {
     // 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 TaxonomyWebTestCase {
     $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_controller('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.'));
   }
diff --git a/modules/user/user.module b/modules/user/user.module
index 926e6cc32cf1..0b25f3e44962 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -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'),
+      'controller class' => 'UserController',
+      'base table' => 'users',
+      'fieldable' => TRUE,
       'object keys' => array(
         'id' => 'uid',
       ),
@@ -176,68 +179,29 @@ function user_external_load($authname) {
  * @return
  *   An array of user objects, indexed by uid.
  *
+ * @see entity_load()
  * @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('user', $uids, $conditions, $reset);
+}
 
-    $queried_users = array();
+/**
+ * Controller class for users.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for user objects.
+ */
+class UserController extends DrupalDefaultEntityController {
+  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';
       }
@@ -246,57 +210,30 @@ function user_load_multiple($uids = array(), $conditions = array(), $reset = FAL
       }
     }
 
-    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.
  *
diff --git a/update.php b/update.php
index e794693ad02a..52521d04b0c8 100644
--- a/update.php
+++ b/update.php
@@ -261,7 +261,8 @@ function update_check_requirements() {
 // reaching the PHP memory limit.
 require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
 require_once DRUPAL_ROOT . '/includes/update.inc';
-
+require_once DRUPAL_ROOT . '/includes/common.inc';
+require_once DRUPAL_ROOT . '/includes/entity.inc';
 update_prepare_d7_bootstrap();
 
 // Determine if the current user has access to run update.php.
-- 
GitLab