Commit 3f36af04 authored by Dries's avatar Dries

- Patch #460320 by catch, fago, Frando: standardized, pluggable entity loading...

- Patch #460320 by catch, fago, Frando: standardized, pluggable entity loading for nodes, users, taxonomies, files and comments.
parent c9c962d8
......@@ -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).
*
......
This diff is collapsed.
......@@ -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);
}
/**
......
......@@ -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
......
......@@ -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.
*
......
......@@ -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.
*
......
......@@ -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']);
......
......@@ -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
......
......@@ -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.
......
......@@ -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;
}