From 58b0235a72859aa433d743a9f284504f24664857 Mon Sep 17 00:00:00 2001 From: Dries Buytaert Date: Fri, 5 Dec 2008 22:18:46 +0000 Subject: [PATCH] - Patch #324313 by catch et al: load multiple nodes and terms at once. --- CHANGELOG.txt | 3 + modules/book/book.module | 19 +- modules/book/book.test | 2 +- modules/comment/comment.module | 29 +- modules/dblog/dblog.test | 2 +- modules/filter/filter.test | 2 +- modules/forum/forum.module | 29 +- modules/forum/forum.test | 2 +- modules/node/node.api.php | 60 +++- modules/node/node.module | 270 +++++++++++------- modules/node/node.test | 84 +++++- modules/path/path.module | 14 +- modules/path/path.test | 6 +- modules/poll/poll.module | 54 ++-- modules/poll/poll.test | 2 +- modules/simpletest/drupal_web_test_case.php | 16 ++ modules/simpletest/tests/taxonomy_test.module | 6 +- modules/system/system.test | 2 +- modules/taxonomy/taxonomy.api.php | 17 +- modules/taxonomy/taxonomy.module | 179 +++++++++++- modules/taxonomy/taxonomy.test | 62 +++- modules/translation/translation.test | 4 +- modules/trigger/trigger.test | 4 +- modules/upload/upload.module | 9 +- 24 files changed, 663 insertions(+), 214 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f89c8107607..06cdd17583c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -32,6 +32,9 @@ Drupal 7.0, xxxx-xx-xx (development version) * Redesigned password strength validator. * Redesigned the add content type screen. * Highlight duplicate URL aliases. +- Performance: + * Improved performance on uncached page views by loading multiple core + objects in a single database query. - Documentation: * Hook API documentation now included in Drupal core. - News aggregator: diff --git a/modules/book/book.module b/modules/book/book.module index fad546bda71..a0e0b674f5f 100644 --- a/modules/book/book.module +++ b/modules/book/book.module @@ -705,18 +705,13 @@ function book_build_active_trail($book_link) { /** * Implementation of hook_nodeapi_load(). */ -function book_nodeapi_load(&$node, $teaser, $page) { - // Note - we cannot use book_link_load() because it will call node_load(). - $info['book'] = db_query('SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid = :nid', array( - ':nid' => $node->nid - ))->fetchAssoc(); - - if ($info['book']) { - $info['book']['href'] = $info['book']['link_path']; - $info['book']['title'] = $info['book']['link_title']; - $info['book']['options'] = unserialize($info['book']['options']); - - return $info; +function book_nodeapi_load($nodes, $types) { + $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (" . db_placeholders(array_keys($nodes)) . ")", array_keys($nodes), array('fetch' => PDO::FETCH_ASSOC)); + foreach ($result as $record) { + $nodes[$record['nid']]->book = $record; + $nodes[$record['nid']]->book['href'] = $record['link_path']; + $nodes[$record['nid']]->book['title'] = $record['link_title']; + $nodes[$record['nid']]->book['options'] = unserialize($record['options']); } } diff --git a/modules/book/book.test b/modules/book/book.test index 5843957fd20..df7b2d6fd64 100644 --- a/modules/book/book.test +++ b/modules/book/book.test @@ -146,7 +146,7 @@ class BookTestCase extends DrupalWebTestCase { } // Check to make sure the book node was created. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertNotNull(($node === FALSE ? NULL : $node), t('Book node found in database.')); $number++; diff --git a/modules/comment/comment.module b/modules/comment/comment.module index b0194031935..759bcc9f3fc 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -579,11 +579,32 @@ function comment_form_alter(&$form, $form_state, $form_id) { /** * Implementation of hook_nodeapi_load(). */ -function comment_nodeapi_load(&$node, $arg = 0) { - if ($node->comment != COMMENT_NODE_DISABLED) { - return db_query('SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchAssoc(); +function comment_nodeapi_load($nodes, $types) { + $comments_enabled = array(); + + // Check if comments are enabled for each node. If comments are disabled, + // assign values without hitting the database. + foreach ($nodes as $node) { + // Store whether comments are enabled for this node. + if ($node->comment != COMMENT_NODE_DISABLED) { + $comments_enabled[] = $node->nid; + } + else { + $node->last_comment_timestamp = $node->created; + $node->last_comment_name = ''; + $node->comment_count = 0; + } + } + + // For nodes with comments enabled, fetch information from the database. + if (!empty($comments_enabled)) { + $result = db_query('SELECT nid, last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid IN(' . db_placeholders($comments_enabled) . ')', $comments_enabled); + foreach ($result as $record) { + $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp; + $nodes[$record->nid]->last_comment_name = $record->last_comment_name; + $nodes[$record->nid]->comment_count = $record->comment_count; + } } - return array('last_comment_timestamp' => $node->created, 'last_comment_name' => '', 'comment_count' => 0); } /** diff --git a/modules/dblog/dblog.test b/modules/dblog/dblog.test index c95ae655291..5e151c232ce 100644 --- a/modules/dblog/dblog.test +++ b/modules/dblog/dblog.test @@ -262,7 +262,7 @@ class DBLogTestCase extends DrupalWebTestCase { $this->drupalPost('node/add/' . $type, $edit, t('Save')); $this->assertResponse(200); // Retrieve node object. - $node = node_load(array('title' => $title)); + $node = $this->drupalGetNodeByTitle($title); $this->assertTrue($node != null, t('Node @title was loaded', array('@title' => $title))); // Edit node. $edit = $this->getContentUpdate($type); diff --git a/modules/filter/filter.test b/modules/filter/filter.test index 331af74a699..3015bf91e37 100644 --- a/modules/filter/filter.test +++ b/modules/filter/filter.test @@ -114,7 +114,7 @@ class FilterAdminTestCase extends DrupalWebTestCase { $this->drupalPost('node/add/page', $edit, t('Save')); $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Filtered node created.')); - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue($node, t('Node found in database.')); $this->drupalGet('node/' . $node->nid); diff --git a/modules/forum/forum.module b/modules/forum/forum.module index 8473ddde532..ee3b2df982a 100644 --- a/modules/forum/forum.module +++ b/modules/forum/forum.module @@ -341,11 +341,25 @@ function forum_nodeapi_delete(&$node, $teaser, $page) { /** * Implementation of hook_nodeapi_load(). */ -function forum_nodeapi_load(&$node, $teaser, $page) { +function forum_nodeapi_load($nodes, $types) { $vid = variable_get('forum_nav_vocabulary', ''); + // If no forum vocabulary is set up, return. + if ($vid == '') { + return; + } $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_nodeapi_check_node_type($node, $vocabulary)) { - return db_fetch_array(db_query('SELECT tid AS forum_tid FROM {forum} WHERE vid = %d', $node->vid)); + + $node_vids = array(); + foreach ($nodes as $node) { + if (isset($vocabulary->nodes[$node->type])) { + $node_vids[] = $node->vid; + } + } + if (!empty($node_vids)) { + $result = db_query('SELECT nid, tid FROM {forum} WHERE vid IN(' . db_placeholders($node_vids) . ')', $node_vids); + foreach ($result as $record) { + $nodes[$record->nid]->forum_tid = $record->tid; + } } } @@ -451,15 +465,6 @@ function forum_form_alter(&$form, $form_state, $form_id) { } } -/** - * Implementation of hook_load(). - */ -function forum_load($node) { - $forum = db_fetch_object(db_query('SELECT * FROM {term_node} WHERE vid = %d', $node->vid)); - - return $forum; -} - /** * Implementation of hook_block(). * diff --git a/modules/forum/forum.test b/modules/forum/forum.test index 1ca169c5782..01ec526d117 100644 --- a/modules/forum/forum.test +++ b/modules/forum/forum.test @@ -236,7 +236,7 @@ class ForumTestCase extends DrupalWebTestCase { } // Retrieve node object. - $node = node_load(array('title' => $title), null, true); // Are these last two parameters necessary? + $node = $this->drupalGetNodeByTitle($title); $this->assertTrue($node != null, t('Node @title was loaded', array('@title' => $title))); // View forum topic. diff --git a/modules/node/node.api.php b/modules/node/node.api.php index 9bf2816edae..6fc5659e9bc 100644 --- a/modules/node/node.api.php +++ b/modules/node/node.api.php @@ -154,6 +154,39 @@ function hook_node_operations() { return $operations; } +/** + * Act on node objects when loaded. + * + * This hook allows you to add information to node objects when loaded from + * the database. It takes an array of nodes indexed by nid as its first + * parameter. For performance reasons, information for all available nodes + * should be loaded in a single query where possible. + * + * The types of all nodes being passed in are also available in the $types + * parameter. If your module keeps track of the node types it supports, this + * allows for an early return if nothing needs to be done. + * + * Due to the internal cache in node_load_multiple(), you should not use this + * hook to modify information returned from the {node} table itself, since + * this may affect the way nodes are returned from the cache in subsequent + * calls to the function. + * + * @see comment_nodeapi_load() + * @see taxonomy_nodeapi_load() + * @see forum_nodeapi_load() + * + * @param $nodes + * An array of node objects indexed by nid. + * @param $types + * An array containing the types of the nodes. + */ +function hook_nodeapi_load($nodes, $types) { + $result = db_query('SELECT nid, foo FROM {mytable} WHERE nid IN(' . db_placeholders(array_keys($nodes)) . ')', array_keys($nodes)); + foreach ($result as $record) { + $nodes[$record->nid]->foo = $record->foo; + } +} + /** * Act on nodes defined by other modules. * @@ -521,25 +554,22 @@ function hook_insert($node) { * Load node-type-specific information. * * This is a hook used by node modules. It is called to allow the module - * a chance to load extra information that it stores about a node, or - * possibly replace already loaded information - which can be dangerous. + * a chance to load extra information that it stores about a node. The hook + * should not be used to replace information from the core {node} table since + * this may interfere with the way nodes are fetched from cache. * - * @param $node - * The node being loaded. At call time, node.module has already loaded - * the basic information about the node, such as its node ID (nid), - * title, and body. - * @return - * An object containing properties of the node being loaded. This will - * be merged with the passed-in $node to result in an object containing - * a set of properties resulting from adding the extra properties to - * the passed-in ones, and overwriting the passed-in ones with the - * extra properties if they have the same name as passed-in properties. + * @param $nodes + * An array of the nodes being loaded, keyed by nid. At call time, + * node.module has already loaded the basic information about the nodes, such + * as node ID (nid), title, and body. * * For a detailed usage example, see node_example.module. */ -function hook_load($node) { - $additions = db_fetch_object(db_query('SELECT * FROM {mytable} WHERE vid = %d', $node->vid)); - return $additions; +function hook_load($nodes) { + $result = db_fetch_object(db_query('SELECT nid, foo FROM {mytable} WHERE nid IN (' . db_placeholders(array_keys($nodes)) . ')', array_keys($nodes))); + foreach ($result as $record) { + $nodes[$record->nid]->foo = $record->foo; + } } /** diff --git a/modules/node/node.module b/modules/node/node.module index 8fb6f456b69..634da3987cd 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -729,94 +729,178 @@ function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { return $return; } +/** + * Load node objects 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. + * + * @param $nids + * An array of node IDs. + * @param $conditions + * An array of conditions on the {node} table in the form 'field' => $value. + * @param $reset + * Whether to reset the internal node_load cache. + * + * @return + * An array of node objects indexed by nid. + */ +function node_load_multiple($nids = array(), $conditions = array(), $reset = FALSE) { + static $node_cache = 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 behaviour 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'); + } + $query->join('users', 'u', 'u.uid = n.uid'); + + // Add fields from the {node} table. + $node_fields = drupal_schema_fields_sql('node'); + + // vid and title are provided by node_revision, so remove them. + unset($node_fields['vid']); + unset($node_fields['title']); + $query->fields('n', $node_fields); + + // Add all fields from the {node_revision} table. + $node_revision_fields = drupal_schema_fields_sql('node_revision'); + + // nid is provided by node, so remove it. + unset($node_revision_fields['nid']); + + // Change timestamp to revision_timestamp before adding it to the query. + unset($node_revision_fields['timestamp']); + $query->addField('r', 'timestamp', 'revision_timestamp'); + $query->fields('r', $node_revision_fields); + + // Add fields from the {users} table. + $user_fields = array('name', 'picture', 'data'); + $query->fields('u', $user_fields); + + if ($nids) { + $query->condition('n.nid', $nids, 'IN'); + } + if ($conditions) { + foreach ($conditions as $field => $value) { + $query->condition('n.' . $field, $value); + } + } + $queried_nodes = $query->execute()->fetchAllAssoc('nid'); + } + + // Pass all nodes loaded from the database through the node type specific + // callbacks and hook_nodeapi_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_get_types('base', $type) . '_load'; + $function($nodes_of_type); + } + } + + // Call hook_nodeapi_load(), pass the node types so modules can return early + // if not acting on types in the array. + foreach (module_implements('nodeapi_load') as $module) { + $function = $module . '_nodeapi_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; +} + /** * Load a node object from the database. * - * @param $param - * Either the nid of the node or an array of conditions to match against in the database query - * @param $revision - * Which numbered revision to load. Defaults to the current version. + * @param $nid + * The node ID. + * @param $vid + * The revision ID. * @param $reset * Whether to reset the internal node_load cache. * * @return * A fully-populated node object. */ -function node_load($param = array(), $revision = NULL, $reset = NULL) { - static $nodes = array(); +function node_load($nid, $vid = array(), $reset = FALSE) { + $vid = isset($vid) ? array('vid' => $vid) : NULL; + $node = node_load_multiple(array($nid), $vid, $reset); - if ($reset) { - $nodes = array(); - } - - $cachable = ($revision == NULL); - $arguments = array(); - if (is_numeric($param)) { - if ($cachable) { - // Is the node statically cached? - if (isset($nodes[$param])) { - return is_object($nodes[$param]) ? clone $nodes[$param] : $nodes[$param]; - } - } - $cond = 'n.nid = %d'; - $arguments[] = $param; - } - elseif (is_array($param)) { - // Turn the conditions into a query. - foreach ($param as $key => $value) { - $cond[] = 'n.' . db_escape_table($key) . " = '%s'"; - $arguments[] = $value; - } - $cond = implode(' AND ', $cond); - } - else { - return FALSE; - } - - // Retrieve a field list based on the site's schema. - $fields = drupal_schema_fields_sql('node', 'n'); - $fields = array_merge($fields, drupal_schema_fields_sql('node_revision', 'r')); - $fields = array_merge($fields, array('u.name', 'u.picture', 'u.data')); - // Remove fields not needed in the query: n.vid and r.nid are redundant, - // n.title is unnecessary because the node title comes from the - // node_revisions table. We'll keep r.vid, r.title, and n.nid. - $fields = array_diff($fields, array('n.vid', 'n.title', 'r.nid')); - $fields = implode(', ', $fields); - // Rename timestamp field for clarity. - $fields = str_replace('r.timestamp', 'r.timestamp AS revision_timestamp', $fields); - // Change name of revision uid so it doesn't conflict with n.uid. - $fields = str_replace('r.uid', 'r.uid AS revision_uid', $fields); - - // Retrieve the node. - // No db_rewrite_sql is applied so as to get complete indexing for search. - if ($revision) { - array_unshift($arguments, $revision); - $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revision} r ON r.nid = n.nid AND r.vid = %d WHERE ' . $cond, $arguments)); - } - else { - $node = db_fetch_object(db_query('SELECT ' . $fields . ' FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revision} r ON r.vid = n.vid WHERE ' . $cond, $arguments)); - } - - if ($node && $node->nid) { - // Call the node specific callback (if any) and piggy-back the - // results to the node or overwrite some values. - if ($extra = node_invoke($node, 'load')) { - foreach ($extra as $key => $value) { - $node->$key = $value; - } - } - - if ($extra = node_invoke_nodeapi($node, 'load')) { - foreach ($extra as $key => $value) { - $node->$key = $value; - } - } - if ($cachable) { - $nodes[$node->nid] = is_object($node) ? clone $node : $node; - } - } - - return $node; + return $node ? $node[$nid] : FALSE; } /** @@ -1740,22 +1824,18 @@ function node_feed($nids = FALSE, $channel = array()) { global $base_url, $language; if ($nids === FALSE) { - $nids = array(); - $result = db_query_range(db_rewrite_sql('SELECT n.nid, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, variable_get('feed_default_items', 10)); - while ($row = db_fetch_object($result)) { - $nids[] = $row->nid; - } + $nids = db_query_range(db_rewrite_sql('SELECT n.nid, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, variable_get('feed_default_items', 10))->fetchCol(); } $item_length = variable_get('feed_item_length', 'teaser'); $namespaces = array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'); + // Load all nodes to be rendered. + $nodes = node_load_multiple($nids); $items = ''; - foreach ($nids as $nid) { - // Load the specified node: - $item = node_load($nid); + foreach ($nodes as $item) { $item->build_mode = NODE_BUILD_RSS; - $item->link = url("node/$nid", array('absolute' => TRUE)); + $item->link = url("node/$item->nid", array('absolute' => TRUE)); if ($item_length != 'title') { $teaser = ($item_length == 'teaser'); @@ -1822,16 +1902,14 @@ function node_feed($nids = FALSE, $channel = array()) { * Menu callback; Generate a listing of promoted nodes. */ function node_page_default() { - $result = pager_query(db_rewrite_sql('SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10)); + $nids = pager_query(db_rewrite_sql('SELECT n.nid FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10))->fetchCol(); + if (!empty($nids)) { + $nodes = node_load_multiple($nids); + $output = ''; + foreach ($nodes as $node) { + $output .= node_view($node, TRUE); + } - $output = ''; - $num_rows = FALSE; - while ($node = db_fetch_object($result)) { - $output .= node_view(node_load($node->nid), 1); - $num_rows = TRUE; - } - - if ($num_rows) { $feed_url = url('rss.xml', array('absolute' => TRUE)); drupal_add_feed($feed_url, variable_get('site_name', 'Drupal') . ' ' . t('RSS')); $output .= theme('pager', NULL, variable_get('default_nodes_main', 10)); diff --git a/modules/node/node.test b/modules/node/node.test index f45225922b2..7f5e5c30304 100644 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -1,6 +1,86 @@ t('Load multiple nodes'), + 'description' => t('Test the loading of multiple nodes.'), + 'group' => t('Node'), + ); + } + + function setUp() { + parent::setUp(); + $web_user = $this->drupalCreateUser(array('create article content', 'create page content')); + $this->drupalLogin($web_user); + } + + /** + * Create four nodes and ensure they're loaded correctly. + */ + function testNodeMultipleLoad() { + $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $node2 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 0)); + $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0)); + + // Confirm that promoted nodes appear in the default node listing. + $this->drupalGet('node'); + $this->assertText($node1->title, t('Node title appears on the default listing.')); + $this->assertText($node2->title, t('Node title appears on the default listing.')); + $this->assertNoText($node3->title, t('Node title does not appear in the default listing.')); + $this->assertNoText($node4->title, t('Node title does not appear in the default listing.')); + + // Load nodes with only a condition. Nodes 3 and 4 will be loaded. + $nodes = node_load_multiple(NULL, array('promote' => 0)); + $this->assertEqual($node3->title, $nodes[$node3->nid]->title, t('Node was loaded.')); + $this->assertEqual($node4->title, $nodes[$node4->nid]->title, t('Node was loaded.')); + $count = count($nodes); + $this->assertTrue($count == 2, t('@count nodes loaded.', array('@count' => $count))); + + // Load nodes by nid. Nodes 1, 2 and 4 will be loaded. + $nodes = node_load_multiple(array(1, 2, 4)); + $count = count($nodes); + $this->assertTrue(count($nodes) == 3, t('@count nodes loaded', array('@count' => $count))); + $this->assertTrue(isset($nodes[$node1->nid]), t('Node is correctly keyed in the array')); + $this->assertTrue(isset($nodes[$node2->nid]), t('Node is correctly keyed in the array')); + $this->assertTrue(isset($nodes[$node4->nid]), t('Node is correctly keyed in the array')); + foreach ($nodes as $node) { + $this->assertTrue(is_object($node), t('Node is an object')); + } + + // Load nodes by nid, where type = article. Nodes 1, 2 and 3 will be loaded. + $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article')); + $count = count($nodes); + $this->assertTrue($count == 3, t('@count nodes loaded', array('@count' => $count))); + $this->assertEqual($nodes[$node1->nid]->title, $node1->title, t('Node successfully loaded.')); + $this->assertEqual($nodes[$node2->nid]->title, $node2->title, t('Node successfully loaded.')); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded.')); + $this->assertFalse(isset($nodes[$node4->nid])); + + // Now that all nodes have been loaded into the static cache, ensure that + // they are loaded correctly again when a condition is passed. + $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article')); + $count = count($nodes); + $this->assertTrue($count == 3, t('@count nodes loaded.', array('@count' => $count))); + $this->assertEqual($nodes[$node1->nid]->title, $node1->title, t('Node successfully loaded')); + $this->assertEqual($nodes[$node2->nid]->title, $node2->title, t('Node successfully loaded')); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded')); + $this->assertFalse(isset($nodes[$node4->nid]), t('Node was not loaded')); + + // Load nodes by nid, where type = article and promote = 0. + $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article', 'promote' => 0)); + $count = count($nodes); + $this->assertTrue($count == 1, t('@count node loaded', array('@count' => $count))); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded.')); + } +} + class NodeRevisionsTestCase extends DrupalWebTestCase { protected $nodes; protected $logs; @@ -258,7 +338,7 @@ class PageEditTestCase extends DrupalWebTestCase { $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the node exists in the database. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue($node, t('Node found in database.')); // Check that "edit" link points to correct page. @@ -352,7 +432,7 @@ class PageCreationTestCase extends DrupalWebTestCase { $this->assertRaw(t('!post %title has been created.', array('!post' => 'Page', '%title' => $edit['title'])), t('Page created.')); // Check that the node exists in the database. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue($node, t('Node found in database.')); } } diff --git a/modules/path/path.module b/modules/path/path.module index bdc4b2ee519..449b72aba4d 100644 --- a/modules/path/path.module +++ b/modules/path/path.module @@ -135,12 +135,14 @@ function path_nodeapi_validate(&$node, $arg) { /** * Implementation of hook_nodeapi_load(). */ -function path_nodeapi_load(&$node, $arg) { - $language = isset($node->language) ? $node->language : ''; - $path = 'node/' . $node->nid; - $alias = drupal_get_path_alias($path, $language); - if ($path != $alias) { - $node->path = $alias; +function path_nodeapi_load($nodes, $types) { + foreach ($nodes as $node) { + $language = isset($node->language) ? $node->language : ''; + $path = 'node/' . $node->nid; + $alias = drupal_get_path_alias($path, $language); + if ($path != $alias) { + $node->path = $alias; + } } } diff --git a/modules/path/path.test b/modules/path/path.test index 40023996d37..6479abc0398 100644 --- a/modules/path/path.test +++ b/modules/path/path.test @@ -131,7 +131,7 @@ class PathTestCase extends DrupalWebTestCase { $this->drupalPost('node/add/page', $edit, t('Save')); // Check to make sure the node was created. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertNotNull(($node === FALSE ? NULL : $node), 'Node found in database. %s'); @@ -188,7 +188,7 @@ class PathLanguageTestCase extends DrupalWebTestCase { $this->drupalPost('node/add/page', $edit, t('Save')); // Check to make sure the node was created. - $english_node = node_load(array('title' => $edit['title'])); + $english_node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue(($english_node), 'Node found in database.'); // Confirm that the alias works. @@ -209,7 +209,7 @@ class PathLanguageTestCase extends DrupalWebTestCase { // Ensure the node was created. // Check to make sure the node was created. - $french_node = node_load(array('title' => $edit['title'])); + $french_node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue(($french_node), 'Node found in database.'); // Confirm that the alias works. diff --git a/modules/poll/poll.module b/modules/poll/poll.module index 2cbd8edcdc2..52afd097386 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -148,11 +148,9 @@ function poll_block($op = 'list', $delta = '') { } elseif ($op == 'view') { // Retrieve the latest poll. - $sql = db_rewrite_sql("SELECT MAX(n.created) FROM {node} n INNER JOIN {poll} p ON p.nid = n.nid WHERE n.status = 1 AND p.active = 1"); - $timestamp = db_result(db_query($sql)); - if ($timestamp) { - $poll = node_load(array('type' => 'poll', 'created' => $timestamp, 'status' => 1)); - + $record = db_query_range(db_rewrite_sql("SELECT n.nid FROM {node} n INNER JOIN {poll} p ON p.nid = n.nid WHERE n.status = :status AND p.active = :active ORDER BY n.created DESC"), array(':status' => 1, ':active' => 1), 0, 1)->fetch(); + if ($record) { + $poll = node_load($record->nid); if ($poll->nid) { $poll = poll_view($poll, TRUE, FALSE, TRUE); } @@ -451,35 +449,35 @@ function poll_validate($node) { /** * Implementation of hook_load(). */ -function poll_load($node) { +function poll_load($nodes) { global $user; + foreach ($nodes as $node) { + $poll = db_query("SELECT runtime, active FROM {poll} WHERE nid = :nid", array(':nid' => $node->nid))->fetch(); - $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid)); + // Load the appropriate choices into the $poll object. + $poll->choice = db_query("SELECT chid, chtext, chvotes, weight FROM {poll_choice} WHERE nid = :nid ORDER BY weight", array(':nid' => $node->nid))->fetchAllAssoc('chid', PDO::FETCH_ASSOC); - // Load the appropriate choices into the $poll object. - $result = db_query("SELECT chid, chtext, chvotes, weight FROM {poll_choice} WHERE nid = %d ORDER BY weight", $node->nid); - while ($choice = db_fetch_array($result)) { - $poll->choice[$choice['chid']] = $choice; - } - - // Determine whether or not this user is allowed to vote. - $poll->allowvotes = FALSE; - if (user_access('vote on polls') && $poll->active) { - if ($user->uid) { - $result = db_fetch_object(db_query('SELECT chid FROM {poll_vote} WHERE nid = %d AND uid = %d', $node->nid, $user->uid)); + // Determine whether or not this user is allowed to vote. + $poll->allowvotes = FALSE; + if (user_access('vote on polls') && $poll->active) { + if ($user->uid) { + $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch(); + } + else { + $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch(); + } + if ($result) { + $poll->vote = $result->chid; + } + else { + $poll->vote = -1; + $poll->allowvotes = TRUE; + } } - else { - $result = db_fetch_object(db_query("SELECT chid FROM {poll_vote} WHERE nid = %d AND hostname = '%s'", $node->nid, ip_address())); - } - if (isset($result->chid)) { - $poll->vote = $result->chid; - } - else { - $poll->vote = -1; - $poll->allowvotes = TRUE; + foreach ($poll as $key => $value) { + $nodes[$node->nid]->$key = $value; } } - return $poll; } /** diff --git a/modules/poll/poll.test b/modules/poll/poll.test index e91f689c7ca..83ac6177896 100644 --- a/modules/poll/poll.test +++ b/modules/poll/poll.test @@ -40,7 +40,7 @@ class PollTestCase extends DrupalWebTestCase { } $this->drupalPost(NULL, $edit, t('Save')); - $node = node_load(array('title' => $title)); + $node = $this->drupalGetNodeByTitle($title); $this->assertRaw(t('@type %title has been created.', array('@type' => node_get_types('name', 'poll'), '%title' => $title)), 'Poll has been created.'); $this->assertTrue($node->nid, t('Poll has been found in the database')); diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 8fecb9ea03e..9660d448302 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -446,6 +446,22 @@ class DrupalWebTestCase { $this->error($exception->getMessage(), 'Uncaught exception', _drupal_get_last_caller($backtrace)); } + /** + * Get a node from the database based on its title. + * + * @param title + * A node title, usually generated by $this->randomName(). + * + * @return + * A node object matching $title. + */ + function drupalGetNodeByTitle($title) { + $nodes = node_load_multiple(array(), array('title' => $title)); + // Load the first node returned from the database. + $returned_node = reset($nodes); + return $returned_node; + } + /** * Creates a node based on default settings. * diff --git a/modules/simpletest/tests/taxonomy_test.module b/modules/simpletest/tests/taxonomy_test.module index 2bbcf24531e..d90148fa2b2 100644 --- a/modules/simpletest/tests/taxonomy_test.module +++ b/modules/simpletest/tests/taxonomy_test.module @@ -9,8 +9,10 @@ /** * Implementation of hook_taxonomy_term_load(). */ -function taxonomy_test_taxonomy_term_load($term) { - $term->antonyms = taxonomy_test_get_antonyms($term->tid); +function taxonomy_test_taxonomy_term_load(&$terms) { + foreach ($terms as $term) { + $term->antonyms = taxonomy_test_get_antonyms($term->tid); + } } /** diff --git a/modules/system/system.test b/modules/system/system.test index a88cef45713..7e039428b5d 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -570,7 +570,7 @@ class PageTitleFiltering extends DrupalWebTestCase { // Create the node with HTML in the title. $this->drupalPost('node/add/page', $edit, t('Save')); - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertNotNull($node, 'Node created and found in database'); $this->drupalGet("node/" . $node->nid); $this->assertText(check_plain($edit['title']), 'Check to make sure tags in the node title are converted.'); diff --git a/modules/taxonomy/taxonomy.api.php b/modules/taxonomy/taxonomy.api.php index 553386000de..fd6d2bcd7b3 100644 --- a/modules/taxonomy/taxonomy.api.php +++ b/modules/taxonomy/taxonomy.api.php @@ -74,12 +74,21 @@ function hook_taxonomy_vocabulary_delete($vocabulary) { * * Modules implementing this hook can act on the term object returned by * taxonomy_term_load(). + * For performance reasons, information to be added to term objects should be + * loaded in a single query for all terms where possible. * - * @param $term - * A taxonomy term object. + * Since terms are stored and retrieved from cache during a page request, avoid + * altering properties provided by the {term_data} table, since this may + * affect the way results are loaded from cache in subsequent calls. + * + * @param $terms + * An array of term objects, indexed by tid. */ -function hook_taxonomy_term_load($term) { - $term->synonyms = taxonomy_get_synonyms($term->tid); +function hook_taxonomy_term_load($terms) { + $result = db_query('SELECT tid, foo FROM {mytable} WHERE tid IN (' . db_placeholders(array_keys($terms)) . ')', array_keys($terms)); + foreach ($result as $record) { + $terms[$record->tid]->foo = $record->foo; + } } /** diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index 958f837739a..eaddef26d32 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -622,6 +622,33 @@ function taxonomy_node_get_terms_by_vocabulary($node, $vid, $key = 'tid') { return $terms; } +/** + * Find all term IDs associated with a set of nodes. + * + * @param $nodes + * An array of node objects. + * + * @return + * An array of term and node IDs ordered by vocabulary and term weight. + */ +function taxonomy_get_tids_from_nodes($nodes) { + $node_vids = array(); + foreach ($nodes as $node) { + $node_vids[] = $node->vid; + } + $query = db_select('term_node', 'r'); + $query->fields('r', array('tid', 'nid', 'vid')); + $query->join('term_data', 't', 'r.tid = t.tid'); + $query->join('vocabulary', 'v', 't.vid = v.vid'); + $query->condition('r.vid', $node_vids, 'IN'); + $query->orderBy('v.weight'); + $query->orderBy('t.weight'); + $query->orderBy('t.name'); + $query->addTag('term_access'); + + return $query->execute()->fetchAll(); +} + /** * Find all terms associated with the given node, ordered by vocabulary and term weight. */ @@ -1049,25 +1076,125 @@ function taxonomy_terms_load($str_tids) { return $terms; } +/** + * Load multiple taxonomy terms based on certain conditions. + * + * This function should be used whenever you need to load more than one term + * from the database. Terms are loaded into memory and will not require + * database access if loaded again during the same page request. + * + * @param $tids + * An array of taxonomy term IDs. + * @param $conditions + * An array of conditions to add to the query. + * @param $reset + * Whether to reset the internal cache. + * + * @return + * An array of term objects, indexed by tid. + */ +function taxonomy_term_load_multiple($tids = array(), $conditions = array(), $reset = FALSE) { + static $term_cache = array(); + + if ($reset) { + $term_cache = array(); + } + + $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) { + foreach ($terms as $term) { + $term_values = (array) $term; + if (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('term_data', 't'); + $term_data = drupal_schema_fields_sql('term_data'); + $query->fields('t', $term_data); + + // If the $tids array is populated, add those to the query. + if ($tids) { + $query->condition('t.tid', $tids, 'IN'); + } + + // If the conditions array is populated, add those to the query. + if ($conditions) { + foreach ($conditions as $field => $value) { + $query->conditions('t.' . $field, $value); + } + } + $queried_terms = $query->execute()->fetchAllAssoc('tid'); + // Invoke hook_taxonomy_term_load() on the terms loaded from the database + // and add them to the static cache. + if (!empty($queried_terms)) { + foreach (module_implements('taxonomy_term_load') as $module) { + $function = $module . '_taxonomy_term_load'; + $function($queried_terms); + } + $terms += $queried_terms; + $term_cache += $queried_terms; + } + } + + // 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; + } + + return $terms; +} + /** * Return the term object matching a term ID. * * @param $tid * A term's ID + * @param $reset + * Whether to reset the static cache. * - * @return Object + * @return * A term object. Results are statically cached. */ function taxonomy_term_load($tid, $reset = FALSE) { if (!is_numeric($tid)) { return FALSE; } - static $terms = array(); - if (!isset($terms[$tid]) || $reset) { - $terms[$tid] = taxonomy_get_term_data($tid, $reset); - module_invoke_all('taxonomy_term_load', $terms[$tid]); - } - return $terms[$tid]; + $term = taxonomy_term_load_multiple(array($tid), array(), $reset); + return $term ? $term[$tid] : FALSE; } /** @@ -1193,12 +1320,16 @@ function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $p */ function taxonomy_render_nodes($result) { $output = ''; - $has_rows = FALSE; - while ($node = db_fetch_object($result)) { - $output .= node_view(node_load($node->nid), 1); - $has_rows = TRUE; + $nids = array(); + foreach ($result as $record) { + $nids[] = $record->nid; } - if ($has_rows) { + if (!empty($nids)) { + $nodes = node_load_multiple($nids); + + foreach ($nodes as $node) { + $output .= node_view($node, 1); + } $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0); } else { @@ -1210,9 +1341,27 @@ function taxonomy_render_nodes($result) { /** * Implementation of hook_nodeapi_load(). */ -function taxonomy_nodeapi_load($node, $arg = 0) { - $output['taxonomy'] = taxonomy_node_get_terms($node); - return $output; +function taxonomy_nodeapi_load($nodes) { + // Get an array of tid, vid associations ordered by vocabulary and term + // weight. + $tids = taxonomy_get_tids_from_nodes($nodes); + + // Extract the tids only from this array. + $term_ids = array(); + foreach ($tids as $term) { + $term_ids[$term->tid] = $term->tid; + } + + // Load the full term objects for these tids. + $terms = taxonomy_term_load_multiple($term_ids); + foreach ($tids as $term) { + $nodes[$term->nid]->taxonomy[$term->tid] = $terms[$term->tid]; + } + foreach ($nodes as $node) { + if (!isset($nodes[$node->nid]->taxonomy)) { + $node->taxonomy = array(); + } + } } /** diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test index bc85bbe683d..9f36b680e81 100644 --- a/modules/taxonomy/taxonomy.test +++ b/modules/taxonomy/taxonomy.test @@ -344,7 +344,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { $this->drupalPost('node/add/article', $edit, t('Save')); // Check that the term is displayed when the node is viewed. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->drupalGet('node/' . $node->nid); $this->assertText($term1->name, t('Term is displayed when viewing the node.')); @@ -433,3 +433,63 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { $this->assertText($edit['description'], t('The randomly generated term description is present.')); } } + +/** + * Test the taxonomy_term_load_multiple() function. + */ +class TaxonomyLoadMultipleUnitTest extends TaxonomyWebTestCase { + + function getInfo() { + return array( + 'name' => t('Taxonomy term multiple loading'), + 'description' => t('Test the loading of multiple taxonomy terms at once'), + 'group' => t('Taxonomy'), + ); + } + + function setUp() { + parent::setUp(); + $this->taxonomy_admin = $this->drupalCreateUser(array('administer taxonomy')); + $this->drupalLogin($this->taxonomy_admin); + } + + /** + * Create a vocabulary and some taxonomy terms, ensuring they're loaded + * correctly using taxonomy_term_load_multiple(). + */ + function testTaxonomyTermMultipleLoad() { + // Create a vocabulary. + $vocabulary = $this->createVocabulary(); + + // Create five terms in the vocabulary. + $i = 0; + while ($i < 5) { + $i++; + $this->createTerm($vocabulary->vid); + } + // Load the terms from the vocabulary. + $terms = taxonomy_term_load_multiple(NULL, array('vid' => $vocabulary->vid)); + $count = count($terms); + $this->assertTrue($count == 5, t('Correct number of terms were loaded. !count terms.', array('!count' => $count))); + + // Load the same terms again by tid. + $terms2 = taxonomy_term_load_multiple(array_keys($terms)); + $this->assertTrue($count == count($terms2), t('Five terms were loaded by tid')); + $this->assertEqual($terms, $terms2, t('Both arrays contain the same terms')); + + // Load the terms by tid, with a condition on vid. + $terms3 = taxonomy_term_load_multiple(array_keys($terms2), array('vid' => $vocabulary->vid)); + $this->assertEqual($terms2, $terms3); + + // Remove one term from the array, then delete it. + $deleted = array_shift($terms3); + taxonomy_term_delete($deleted->tid); + $deleted_term = taxonomy_term_load($deleted->tid, TRUE); + $this->assertFalse($deleted_term); + + // Load terms from the vocabulary by vid. + $terms4 = taxonomy_term_load_multiple(NULL, array('vid' => $vocabulary->vid), TRUE); + $this->assertTrue(count($terms4 == 4), t('Correct number of terms were loaded.')); + $this->assertFalse(isset($terms4[$deleted->tid])); + } +} diff --git a/modules/translation/translation.test b/modules/translation/translation.test index e7a97790a60..d8dd43b310f 100644 --- a/modules/translation/translation.test +++ b/modules/translation/translation.test @@ -117,7 +117,7 @@ class TranslationTestCase extends DrupalWebTestCase { $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Page created.')); // Check to make sure the node was created. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue($node, t('Node found in database.')); return $node; @@ -141,7 +141,7 @@ class TranslationTestCase extends DrupalWebTestCase { $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Translation created.')); // Check to make sure that translation was successfull. - $node = node_load(array('title' => $edit['title'])); + $node = $this->drupalGetNodeByTitle($edit['title']); $this->assertTrue($node, t('Node found in database.')); return $node; diff --git a/modules/trigger/trigger.test b/modules/trigger/trigger.test index fab53e5f4da..e114293062a 100644 --- a/modules/trigger/trigger.test +++ b/modules/trigger/trigger.test @@ -44,7 +44,7 @@ class TriggerContentTestCase extends DrupalWebTestCase { // Make sure the text we want appears. $this->assertRaw(t('!post %title has been created.', array ('!post' => 'Page', '%title' => $edit['title'])), t('Make sure the page has actually been created')); // Action should have been fired. - $loaded_node = node_load(array('title' => $edit['title']), NULL, TRUE); + $loaded_node = $this->drupalGetNodeByTitle($edit['title']);; $this->assertTrue($loaded_node->$info['property'] == $info['expected'], t('Make sure the @action action fired.', array('@action' => $info['name']))); // Leave action assigned for next test @@ -108,4 +108,4 @@ class TriggerContentTestCase extends DrupalWebTestCase { ); return $info[$action]; } -} \ No newline at end of file +} diff --git a/modules/upload/upload.module b/modules/upload/upload.module index c4eacc2bd99..fce0b2aff9f 100644 --- a/modules/upload/upload.module +++ b/modules/upload/upload.module @@ -300,10 +300,11 @@ function upload_file_delete(&$file) { /** * Implementation of hook_nodeapi_load(). */ -function upload_nodeapi_load(&$node, $teaser) { - if (variable_get("upload_$node->type", 1) == 1) { - $output = array('files' => upload_load($node)); - return $output; +function upload_nodeapi_load($nodes, $types) { + foreach ($nodes as $node) { + if (variable_get("upload_$node->type", 1) == 1) { + $node->files = upload_load($node); + } } }