diff --git a/modules/comment/comment.admin.inc b/modules/comment/comment.admin.inc index 4f3d350..21fba35 100644 --- a/modules/comment/comment.admin.inc +++ b/modules/comment/comment.admin.inc @@ -253,7 +253,7 @@ function comment_confirm_delete_page($cid) { * @ingroup forms * @see comment_confirm_delete_submit() */ -function comment_confirm_delete($form, &$form_state, $comment) { +function comment_confirm_delete($form, &$form_state, EntityInterface $comment) { $form['#comment'] = $comment; // Always provide entity id in the same form key as in the entity edit form. $form['cid'] = array('#type' => 'value', '#value' => $comment->cid); diff --git a/modules/comment/comment.entity.inc b/modules/comment/comment.entity.inc new file mode 100644 index 0000000..c237025 --- /dev/null +++ b/modules/comment/comment.entity.inc @@ -0,0 +1,257 @@ +innerJoin('node', 'n', 'base.nid = n.nid'); + $query->addField('n', 'type', 'node_type'); + $query->innerJoin('users', 'u', 'base.uid = u.uid'); + $query->addField('u', 'name', 'registered_name'); + $query->fields('u', array('uid', 'signature', 'signature_format', 'picture')); + return $query; + } + + protected function attachLoad(&$comments, $revision_id = FALSE) { + // Setup standard comment properties. + foreach ($comments as $key => $comment) { + $comment->name = $comment->uid ? $comment->registered_name : $comment->name; + $comment->new = node_mark($comment->nid, $comment->changed); + $comment->node_type = 'comment_node_' . $comment->node_type; + $comments[$key] = $comment; + } + parent::attachLoad($comments, $revision_id); + } + + protected function preSave(EntityInterface $comment) { + global $user; + + if (!isset($comment->status)) { + $comment->status = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED; + } + // Make sure we have a proper bundle name. + if (!isset($comment->node_type)) { + $node = node_load($comment->nid); + $comment->node_type = 'comment_node_' . $node->type; + } + if (!$comment->cid) { + // Add the comment to database. This next section builds the thread field. + // Also see the documentation for comment_view(). + if (!empty($comment->thread)) { + // Allow calling code to set thread itself. + $thread = $comment->thread; + } + elseif ($comment->pid == 0) { + // This is a comment with no parent comment (depth 0): we start + // by retrieving the maximum thread level. + $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); + // Strip the "/" from the end of the thread. + $max = rtrim($max, '/'); + // Finally, build the thread field for this new comment. + $thread = comment_int_to_alphadecimal(comment_alphadecimal_to_int($max) + 1) . '/'; + } + else { + // This is a comment with a parent comment, so increase the part of + // the thread value at the proper depth. + + // Get the parent comment: + $parent = comment_load($comment->pid); + // Strip the "/" from the end of the parent thread. + $parent->thread = (string) rtrim((string) $parent->thread, '/'); + // Get the max value in *this* thread. + $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( + ':thread' => $parent->thread . '.%', + ':nid' => $comment->nid, + ))->fetchField(); + + if ($max == '') { + // First child of this parent. + $thread = $parent->thread . '.' . comment_int_to_alphadecimal(0) . '/'; + } + else { + // Strip the "/" at the end of the thread. + $max = rtrim($max, '/'); + // Get the value at the correct depth. + $parts = explode('.', $max); + $parent_depth = count(explode('.', $parent->thread)); + $last = $parts[$parent_depth]; + // Finally, build the thread field for this new comment. + $thread = $parent->thread . '.' . comment_int_to_alphadecimal(comment_alphadecimal_to_int($last) + 1) . '/'; + } + } + if (empty($comment->created)) { + $comment->created = REQUEST_TIME; + } + if (empty($comment->changed)) { + $comment->changed = $comment->created; + } + if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. + $comment->name = $user->name; + } + // Add the values which aren't passed into the function. + $comment->thread = $thread; + $comment->hostname = ip_address(); + } + } + + protected function postSave(EntityInterface $comment) { + // Update the {node_comment_statistics} table prior to executing the hook. + $this->updateNodeStatistics($comment->nid); + // Care about hook_comment_publish(), + if ($comment->status == COMMENT_PUBLISHED) { + module_invoke_all('comment_publish', $comment); + } + } + + protected function postDelete($comments) { + // Delete the comments' replies. + $query = db_select('comment', 'c') + ->fields('c', array('cid')) + ->condition('pid', array(array_keys($comments)), 'IN'); + $child_cids = $query->execute()->fetchCol(); + comment_delete_multiple($child_cids); + + foreach ($comments as $comment) { + $this->updateNodeStatistics($comment->nid); + } + } + + /** + * Updates the comment statistics for a given node. + * + * The following fields are contained in the node_comment_statistics table. + * - last_comment_timestamp: the timestamp of the last comment for this node + * or the node create stamp if no comments exist for the node. + * - last_comment_name: the name of the anonymous poster for the last comment + * - last_comment_uid: the uid of the poster for the last comment for this + * node or the node authors uid if no comments exists for the node. + * - comment_count: the total number of approved/published comments on this + * node. + */ + protected function updateNodeStatistics($nid) { + // Allow bulk updates and inserts to temporarily disable the + // maintenance of the {node_comment_statistics} table. + if (!variable_get('comment_maintain_node_statistics', TRUE)) { + return; + } + + $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchField(); + + if ($count > 0) { + // Comments exist. + $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchObject(); + db_update('node_comment_statistics') + ->fields(array( + 'cid' => $last_reply->cid, + 'comment_count' => $count, + 'last_comment_timestamp' => $last_reply->changed, + 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name, + 'last_comment_uid' => $last_reply->uid, + )) + ->condition('nid', $nid) + ->execute(); + } + else { + // Comments do not exist. + $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); + db_update('node_comment_statistics') + ->fields(array( + 'cid' => 0, + 'comment_count' => 0, + 'last_comment_timestamp' => $node->created, + 'last_comment_name' => '', + 'last_comment_uid' => $node->uid, + )) + ->condition('nid', $nid) + ->execute(); + } + } +} diff --git a/modules/comment/comment.info b/modules/comment/comment.info index 949ffc2..606f46d 100644 --- a/modules/comment/comment.info +++ b/modules/comment/comment.info @@ -5,7 +5,7 @@ version = VERSION core = 8.x dependencies[] = text dependencies[] = entity -files[] = comment.module +files[] = comment.entity.inc files[] = comment.test configure = admin/content/comment stylesheets[all][] = comment.css diff --git a/modules/comment/comment.module b/modules/comment/comment.module index f9af40a..1506e7b 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -98,7 +98,8 @@ function comment_entity_info() { 'base table' => 'comment', 'uri callback' => 'comment_uri', 'fieldable' => TRUE, - 'controller class' => 'CommentController', + 'controller class' => 'CommentStorageController', + 'entity class' => 'Comment', 'entity keys' => array( 'id' => 'cid', 'bundle' => 'node_type', @@ -153,7 +154,7 @@ function comment_node_type_load($name) { /** * Entity uri callback. */ -function comment_uri($comment) { +function comment_uri(EntityInterface $comment) { return array( 'path' => 'comment/' . $comment->cid, 'options' => array('fragment' => 'comment-' . $comment->cid), @@ -738,7 +739,7 @@ function comment_node_page_additions($node) { // Append comment form if needed. if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) { - $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid)); + $build = drupal_get_form("comment_node_{$node->type}_form", entity_construct('comment', array('nid' => $node->nid))); $additions['comment_form'] = $build; } @@ -903,7 +904,7 @@ function comment_prepare_thread(&$comments) { /** * Generate an array for rendering the given comment. * - * @param $comment + * @param EntityInterface $comment * A comment object. * @param $node * The node the comment is attached to. @@ -916,7 +917,7 @@ function comment_prepare_thread(&$comments) { * @return * An array as expected by drupal_render(). */ -function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) { +function comment_view(EntityInterface $comment, $node, $view_mode = 'full', $langcode = NULL) { if (!isset($langcode)) { $langcode = $GLOBALS['language_content']->language; } @@ -973,7 +974,7 @@ function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) { * The content built for the comment (field values, comments, file attachments or * other comment components) will vary depending on the $view_mode parameter. * - * @param $comment + * @param EntityInterface $comment * A comment object. * @param $node * The node the comment is attached to. @@ -983,7 +984,7 @@ function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) { * (optional) A language code to use for rendering. Defaults to the global * content language of the current request. */ -function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) { +function comment_build_content(EntityInterface $comment, $node, $view_mode = 'full', $langcode = NULL) { if (!isset($langcode)) { $langcode = $GLOBALS['language_content']->language; } @@ -1019,14 +1020,14 @@ function comment_build_content($comment, $node, $view_mode = 'full', $langcode = * * Adds reply, edit, delete etc. depending on the current user permissions. * - * @param $comment + * @param EntityInterface $comment * The comment object. * @param $node * The node the comment is attached to. * @return * A structured array of links. */ -function comment_links($comment, $node) { +function comment_links(EntityInterface $comment, $node) { $links = array(); if ($node->comment == COMMENT_NODE_OPEN) { if (user_access('administer comments') && user_access('post comments')) { @@ -1418,12 +1419,12 @@ function comment_user_delete($account) { * @param $op * The operation that is to be performed on the comment. Only 'edit' is * recognized now. - * @param $comment + * @param EntityInterface $comment * The comment object. * @return * TRUE if the current user has acces to the comment, FALSE otherwise. */ -function comment_access($op, $comment) { +function comment_access($op, EntityInterface $comment) { global $user; if ($op == 'edit') { @@ -1434,153 +1435,11 @@ function comment_access($op, $comment) { /** * Accepts a submission of new or changed comment content. * - * @param $comment + * @param EntityInterface $comment * A comment object. - * - * @see comment_int_to_alphadecimal() */ -function comment_save($comment) { - global $user; - - $transaction = db_transaction(); - try { - $defaults = array( - 'mail' => '', - 'homepage' => '', - 'name' => '', - 'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, - ); - foreach ($defaults as $key => $default) { - if (!isset($comment->$key)) { - $comment->$key = $default; - } - } - // Make sure we have a bundle name. - if (!isset($comment->node_type)) { - $node = node_load($comment->nid); - $comment->node_type = 'comment_node_' . $node->type; - } - - // Load the stored entity, if any. - if (!empty($comment->cid) && !isset($comment->original)) { - $comment->original = entity_load_unchanged('comment', $comment->cid); - } - - field_attach_presave('comment', $comment); - - // Allow modules to alter the comment before saving. - module_invoke_all('comment_presave', $comment); - module_invoke_all('entity_presave', $comment, 'comment'); - - if ($comment->cid) { - - drupal_write_record('comment', $comment, 'cid'); - - // Ignore slave server temporarily to give time for the - // saved comment to be propagated to the slave. - db_ignore_slave(); - - // Update the {node_comment_statistics} table prior to executing hooks. - _comment_update_node_statistics($comment->nid); - - field_attach_update('comment', $comment); - // Allow modules to respond to the updating of a comment. - module_invoke_all('comment_update', $comment); - module_invoke_all('entity_update', $comment, 'comment'); - } - else { - // Add the comment to database. This next section builds the thread field. - // Also see the documentation for comment_view(). - if (!empty($comment->thread)) { - // Allow calling code to set thread itself. - $thread = $comment->thread; - } - elseif ($comment->pid == 0) { - // This is a comment with no parent comment (depth 0): we start - // by retrieving the maximum thread level. - $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); - // Strip the "/" from the end of the thread. - $max = rtrim($max, '/'); - // Finally, build the thread field for this new comment. - $thread = comment_increment_alphadecimal($max) . '/'; - } - else { - // This is a comment with a parent comment, so increase the part of the - // thread value at the proper depth. - - // Get the parent comment: - $parent = comment_load($comment->pid); - // Strip the "/" from the end of the parent thread. - $parent->thread = (string) rtrim((string) $parent->thread, '/'); - // Get the max value in *this* thread. - $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( - ':thread' => $parent->thread . '.%', - ':nid' => $comment->nid, - ))->fetchField(); - - if ($max == '') { - // First child of this parent. - $thread = $parent->thread . '.' . comment_int_to_alphadecimal(0) . '/'; - } - else { - // Strip the "/" at the end of the thread. - $max = rtrim($max, '/'); - // Get the value at the correct depth. - $parts = explode('.', $max); - $parent_depth = count(explode('.', $parent->thread)); - $last = $parts[$parent_depth]; - // Finally, build the thread field for this new comment. - $thread = $parent->thread . '.' . comment_increment_alphadecimal($last) . '/'; - } - } - - if (empty($comment->created)) { - $comment->created = REQUEST_TIME; - } - - if (empty($comment->changed)) { - $comment->changed = $comment->created; - } - - if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. - $comment->name = $user->name; - } - - // Ensure the parent id (pid) has a value set. - if (empty($comment->pid)) { - $comment->pid = 0; - } - - // Add the values which aren't passed into the function. - $comment->thread = $thread; - $comment->hostname = ip_address(); - - drupal_write_record('comment', $comment); - - // Ignore slave server temporarily to give time for the - // created comment to be propagated to the slave. - db_ignore_slave(); - - // Update the {node_comment_statistics} table prior to executing hooks. - _comment_update_node_statistics($comment->nid); - - field_attach_insert('comment', $comment); - - // Tell the other modules a new comment has been submitted. - module_invoke_all('comment_insert', $comment); - module_invoke_all('entity_insert', $comment, 'comment'); - } - if ($comment->status == COMMENT_PUBLISHED) { - module_invoke_all('comment_publish', $comment); - } - unset($comment->original); - } - catch (Exception $e) { - $transaction->rollback('comment'); - watchdog_exception('comment', $e); - throw $e; - } - +function comment_save(EntityInterface $comment) { + $comment->save(); } /** @@ -1600,31 +1459,7 @@ function comment_delete($cid) { * The comment to delete. */ function comment_delete_multiple($cids) { - $comments = comment_load_multiple($cids); - if ($comments) { - $transaction = db_transaction(); - try { - // Delete the comments. - db_delete('comment') - ->condition('cid', array_keys($comments), 'IN') - ->execute(); - foreach ($comments as $comment) { - field_attach_delete('comment', $comment); - module_invoke_all('comment_delete', $comment); - module_invoke_all('entity_delete', $comment, 'comment'); - - // Delete the comment's replies. - $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol(); - comment_delete_multiple($child_cids); - _comment_update_node_statistics($comment->nid); - } - } - catch (Exception $e) { - $transaction->rollback(); - watchdog_exception('comment', $e); - throw $e; - } - } + entity_delete_multiple('comment', $cids); } /** @@ -1672,37 +1507,6 @@ function comment_load($cid, $reset = FALSE) { } /** - * Controller class for comments. - * - * This extends the DrupalDefaultEntityController class, adding required - * special handling for comment objects. - */ -class CommentController extends DrupalDefaultEntityController { - - protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { - $query = parent::buildQuery($ids, $conditions, $revision_id); - // Specify additional fields from the user and node tables. - $query->innerJoin('node', 'n', 'base.nid = n.nid'); - $query->addField('n', 'type', 'node_type'); - $query->innerJoin('users', 'u', 'base.uid = u.uid'); - $query->addField('u', 'name', 'registered_name'); - $query->fields('u', array('uid', 'signature', 'signature_format', 'picture')); - return $query; - } - - protected function attachLoad(&$comments, $revision_id = FALSE) { - // Setup standard comment properties. - foreach ($comments as $key => $comment) { - $comment->name = $comment->uid ? $comment->registered_name : $comment->name; - $comment->new = node_mark($comment->nid, $comment->changed); - $comment->node_type = 'comment_node_' . $comment->node_type; - $comments[$key] = $comment; - } - parent::attachLoad($comments, $revision_id); - } -} - -/** * Get number of new comments for current user and specified node. * * @param $nid @@ -1799,7 +1603,7 @@ function comment_get_display_page($cid, $node_type) { /** * Page callback for comment editing. */ -function comment_edit_page($comment) { +function comment_edit_page(EntityInterface $comment) { drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH); $node = node_load($comment->nid); return drupal_get_form("comment_node_{$node->type}_form", $comment); @@ -1824,29 +1628,13 @@ function comment_forms() { * * @ingroup forms */ -function comment_form($form, &$form_state, $comment) { +function comment_form($form, &$form_state, EntityInterface $comment) { global $user; // During initial form build, add the comment entity to the form state for // use during form building and processing. During a rebuild, use what is in // the form state. if (!isset($form_state['comment'])) { - $defaults = array( - 'name' => '', - 'mail' => '', - 'homepage' => '', - 'subject' => '', - 'comment' => '', - 'cid' => NULL, - 'pid' => NULL, - 'language' => LANGUAGE_NONE, - 'uid' => 0, - ); - foreach ($defaults as $key => $value) { - if (!isset($comment->$key)) { - $comment->$key = $value; - } - } $form_state['comment'] = $comment; } else { @@ -2044,7 +1832,7 @@ function comment_form_build_preview($form, &$form_state) { /** * Generate a comment preview. */ -function comment_preview($comment) { +function comment_preview(EntityInterface $comment) { global $user; drupal_set_title(t('Preview comment'), PASS_THROUGH); @@ -2143,13 +1931,7 @@ function comment_form_validate($form, &$form_state) { /** * Prepare a comment for submission. */ -function comment_submit($comment) { - // @todo Legacy support. Remove in Drupal 8. - if (is_array($comment)) { - $comment += array('subject' => ''); - $comment = (object) $comment; - } - +function comment_submit(EntityInterface $comment) { if (empty($comment->date)) { $comment->date = 'now'; } @@ -2400,61 +2182,6 @@ function _comment_per_page() { } /** - * Updates the comment statistics for a given node. This should be called any - * time a comment is added, deleted, or updated. - * - * The following fields are contained in the node_comment_statistics table. - * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node. - * - last_comment_name: the name of the anonymous poster for the last comment - * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node. - * - comment_count: the total number of approved/published comments on this node. - */ -function _comment_update_node_statistics($nid) { - // Allow bulk updates and inserts to temporarily disable the - // maintenance of the {node_comment_statistics} table. - if (!variable_get('comment_maintain_node_statistics', TRUE)) { - return; - } - - $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( - ':nid' => $nid, - ':status' => COMMENT_PUBLISHED, - ))->fetchField(); - - if ($count > 0) { - // Comments exist. - $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array( - ':nid' => $nid, - ':status' => COMMENT_PUBLISHED, - ))->fetchObject(); - db_update('node_comment_statistics') - ->fields(array( - 'cid' => $last_reply->cid, - 'comment_count' => $count, - 'last_comment_timestamp' => $last_reply->changed, - 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name, - 'last_comment_uid' => $last_reply->uid, - )) - ->condition('nid', $nid) - ->execute(); - } - else { - // Comments do not exist. - $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); - db_update('node_comment_statistics') - ->fields(array( - 'cid' => 0, - 'comment_count' => 0, - 'last_comment_timestamp' => $node->created, - 'last_comment_name' => '', - 'last_comment_uid' => $node->uid, - )) - ->condition('nid', $nid) - ->execute(); - } -} - -/** * Generate sorting code. * * Consists of a leading character indicating length, followed by N digits @@ -2530,7 +2257,7 @@ function comment_action_info() { /** * Publishes a comment. * - * @param $comment + * @param EntityInterface $comment * An optional comment object. * @param array $context * Array with components: @@ -2538,7 +2265,7 @@ function comment_action_info() { * * @ingroup actions */ -function comment_publish_action($comment, $context = array()) { +function comment_publish_action(EntityInterface $comment, $context = array()) { if (isset($comment->subject)) { $subject = $comment->subject; $comment->status = COMMENT_PUBLISHED; @@ -2557,7 +2284,7 @@ function comment_publish_action($comment, $context = array()) { /** * Unpublishes a comment. * - * @param $comment + * @param EntityInterface $comment * An optional comment object. * @param array $context * Array with components: @@ -2565,7 +2292,7 @@ function comment_publish_action($comment, $context = array()) { * * @ingroup actions */ -function comment_unpublish_action($comment, $context = array()) { +function comment_unpublish_action(EntityInterface $comment, $context = array()) { if (isset($comment->subject)) { $subject = $comment->subject; $comment->status = COMMENT_NOT_PUBLISHED; @@ -2584,7 +2311,7 @@ function comment_unpublish_action($comment, $context = array()) { /** * Unpublishes a comment if it contains certain keywords. * - * @param $comment + * @param EntityInterface $comment * Comment object to modify. * @param array $context * Array with components: @@ -2595,7 +2322,7 @@ function comment_unpublish_action($comment, $context = array()) { * @see comment_unpublish_by_keyword_action_form() * @see comment_unpublish_by_keyword_action_submit() */ -function comment_unpublish_by_keyword_action($comment, $context) { +function comment_unpublish_by_keyword_action(EntityInterface $comment, $context) { foreach ($context['keywords'] as $keyword) { $text = drupal_render($comment); if (strpos($text, $keyword) !== FALSE) { @@ -2638,7 +2365,7 @@ function comment_unpublish_by_keyword_action_submit($form, $form_state) { * * @ingroup actions */ -function comment_save_action($comment) { +function comment_save_action(EntityInterface $comment) { comment_save($comment); cache_clear_all(); watchdog('action', 'Saved comment %title', array('%title' => $comment->subject)); diff --git a/modules/comment/comment.pages.inc b/modules/comment/comment.pages.inc index 7e88bff..7193fb1 100644 --- a/modules/comment/comment.pages.inc +++ b/modules/comment/comment.pages.inc @@ -35,7 +35,7 @@ function comment_reply($node, $pid = NULL) { // The user is previewing a comment prior to submitting it. if ($op == t('Preview')) { if (user_access('post comments')) { - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) array('pid' => $pid, 'nid' => $node->nid)); + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", entity_construct('comment', array('pid' => $pid, 'nid' => $node->nid))); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); @@ -86,8 +86,8 @@ function comment_reply($node, $pid = NULL) { drupal_goto("node/$node->nid"); } elseif (user_access('post comments')) { - $edit = array('nid' => $node->nid, 'pid' => $pid); - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) $edit); + $comment = entity_construct('comment', array('nid' => $node->nid, 'pid' => $pid)); + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); diff --git a/modules/comment/comment.test b/modules/comment/comment.test index 2e96ba3..cc3dcd4 100644 --- a/modules/comment/comment.test +++ b/modules/comment/comment.test @@ -87,7 +87,7 @@ class CommentHelperCase extends DrupalWebTestCase { } if (isset($match[1])) { - return (object) array('id' => $match[1], 'subject' => $subject, 'comment' => $comment); + return entity_construct('comment', array('id' => $match[1], 'subject' => $subject, 'comment' => $comment)); } } @@ -269,7 +269,7 @@ class CommentHelperCase extends DrupalWebTestCase { // Create a new comment. This helper function may be run with different // comment settings so use comment_save() to avoid complex setup. - $comment = (object) array( + $comment = entity_construct('comment', array( 'cid' => NULL, 'nid' => $this->node->nid, 'node_type' => $this->node->type, @@ -280,7 +280,7 @@ class CommentHelperCase extends DrupalWebTestCase { 'hostname' => ip_address(), 'language' => LANGUAGE_NONE, 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), - ); + )); comment_save($comment); $this->drupalLogout(); @@ -661,7 +661,7 @@ class CommentInterfaceTest extends CommentHelperCase { if ($info['comment count']) { // Create a comment via CRUD API functionality, since // $this->postComment() relies on actual user permissions. - $comment = (object) array( + $comment = entity_construct('comment', array( 'cid' => NULL, 'nid' => $this->node->nid, 'node_type' => $this->node->type, @@ -672,7 +672,7 @@ class CommentInterfaceTest extends CommentHelperCase { 'hostname' => ip_address(), 'language' => LANGUAGE_NONE, 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), - ); + )); comment_save($comment); $this->comment = $comment; @@ -1463,7 +1463,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $anonymous_comment4 = entity_construct('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body)); $this->drupalLogout(); $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.')); @@ -1527,7 +1527,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $anonymous_comment4 = entity_construct('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body)); $this->drupalLogout(); $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.')); diff --git a/modules/entity/entity.class.inc b/modules/entity/entity.class.inc new file mode 100644 index 0000000..001bcdd --- /dev/null +++ b/modules/entity/entity.class.inc @@ -0,0 +1,326 @@ +entityType = $entity_type; + $this->setUp(); + // Set initial values. + foreach ($values as $key => $value) { + $this->$key = $value; + } + } + + /** + * Set up the object instance on construction or unserializiation. + */ + protected function setUp() { + $this->entityInfo = entity_get_info($this->entityType); + $this->idKey = $this->entityInfo['entity keys']['id']; + $this->bundleKey = isset($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : NULL; + } + + public function id() { + return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL; + } + + public function isNew() { + // We support creating entities with pre-defined ids to ease migrations. + // For that the "is_new" property may be set to TRUE. + return !empty($this->is_new) || empty($this->{$this->idKey}); + } + + public function entityType() { + return $this->entityType; + } + + public function bundle() { + return isset($this->bundleKey) ? $this->{$this->bundleKey} : $this->entityType; + } + + public function label() { + // @see entity_label() + $label = FALSE; + if (isset($this->entityInfo['label callback']) && function_exists($this->entityInfo['label callback'])) { + $label = $this->entityInfo['label callback']($this->entityType, $this); + } + elseif (!empty($this->entityInfo['entity keys']['label']) && isset($entity->{$this->entityInfo['entity keys']['label']})) { + $label = $entity->{$this->entityInfo['entity keys']['label']}; + } + return $label; + } + + public function uri() { + // @see entity_uri() + $bundle = $this->bundle(); + // A bundle-specific callback takes precedence over the generic one for the + // entity type. + if (isset($this->entityInfo['bundles'][$bundle]['uri callback'])) { + $uri_callback = $this->entityInfo['bundles'][$bundle]['uri callback']; + } + elseif (isset($this->entityInfo['uri callback'])) { + $uri_callback = $this->entityInfo['uri callback']; + } + else { + return NULL; + } + + // Invoke the callback to get the URI. If there is no callback, return NULL. + if (isset($uri_callback) && function_exists($uri_callback)) { + $uri = $uri_callback($this); + // Pass the entity data to url() so that alter functions do not need to + // lookup this entity again. + $uri['options']['entity_type'] = $this->entityType; + $uri['options']['entity'] = $this; + return $uri; + } + } + + /** + * @todo + * Implement default handling of language. + */ + public function get($property_name, $language = NULL) { + $value = isset($this->$property_name) ? $this->$property_name : NULL; + if (isset($language)) { + return isset($value[$language]) ? $value[$language] : NULL; + } + return $value; + } + + /** + * @todo + * Implement default handling of language. + */ + public function set($property_name, $value, $language = NULL) { + if (isset($language)) { + $this->$property_name[$language] = $value; + } + else { + $this->$property_name = $value; + } + } + + public function save() { + return entity_get_controller($this->entityType)->save($this); + } + + public function delete() { + if (!$this->isNew()) { + entity_get_controller($this->entityType)->delete(array($this->id())); + } + } + + public function createDuplicate() { + $duplicate = clone $this; + $duplicate->{$this->idKey} = NULL; + return $duplicate; + } + + public function entityInfo() { + return $this->entityInfo; + } + + /** + * Magic method to only serialize what is necessary. + */ + public function __sleep() { + $vars = get_object_vars($this); + unset($vars['entityInfo'], $vars['idKey'], $vars['bundleKey']); + // Also key the returned array with the variable names so the method may + // be easily overridden and customized. + return drupal_map_assoc(array_keys($vars)); + } + + /** + * Magic method to invoke setUp() on unserialization. + */ + public function __wakeup() { + $this->setUp(); + } +} + diff --git a/modules/entity/entity.controller.inc b/modules/entity/entity.controller.inc index 8a34207..4d8fc7e 100644 --- a/modules/entity/entity.controller.inc +++ b/modules/entity/entity.controller.inc @@ -194,11 +194,16 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { // 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 ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) { - // Build the query. - $query = $this->buildQuery($ids, $conditions, $revision_id); - $queried_entities = $query - ->execute() - ->fetchAllAssoc($this->idKey); + // Build and execute the query. + $query_result = $this->buildQuery($ids, $conditions, $revision_id)->execute(); + + if (!empty($this->entityInfo['entity class'])) { + // Let PDO create objects of the specified entity class, for which we + // have to provide necessary arguments for constructing the objects. + // @see EntityInterface::__construct() + $query_result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType)); + } + $queried_entities = $query_result->fetchAllAssoc($this->idKey); } // Pass all entities loaded from the database through $this->attachLoad(), @@ -388,3 +393,184 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { $this->entityCache += $entities; } } + +/** + * Interface for entity storage controllers. + */ +interface EntityStorageControllerInterface extends DrupalEntityControllerInterface { + + /** + * Deletes permanently saved entities. + * + * In case of failures, an exception is thrown. + * + * @param $ids + * An array of entity IDs. + */ + public function delete($ids); + + /** + * Saves the entity permanently. + * + * In case of failures, an exception is thrown. + * + * @param EntityInterface $entity + * The entity to save. + * + * @return + * SAVED_NEW or SAVED_UPDATED is returned depending on the operation + * performed. + */ + public function save(EntityInterface $entity); + +} + +/** + * A controller implementing EntityAPIStorageControllerInterface for the database. + */ +class EntityDatabaseStorageController extends DrupalDefaultEntityController implements EntityStorageControllerInterface { + + /** + * Implements EntityStorageControllerInterface. + */ + public function delete($ids) { + $entities = $ids ? $this->load($ids) : FALSE; + if (!$entities) { + // Do nothing, in case invalid or no ids have been passed. + return; + } + $transaction = db_transaction(); + + try { + $this->preDelete($entities); + $ids = array_keys($entities); + + db_delete($this->entityInfo['base table']) + ->condition($this->idKey, $ids, 'IN') + ->execute(); + // Reset the cache as soon as the changes have been applied. + $this->resetCache($ids); + + $this->postDelete($entities); + foreach ($entities as $id => $entity) { + $this->invokeHookDelete($entity); + } + // Ignore slave server temporarily. + db_ignore_slave(); + } + catch (Exception $e) { + if (isset($transaction)) { + $transaction->rollback(); + } + watchdog_exception($this->entityType, $e); + throw $e; + } + } + + /** + * Implements EntityStorageControllerInterface. + */ + public function save(EntityInterface $entity) { + $transaction = db_transaction(); + try { + // Load the stored entity, if any. + if (!$entity->isNew() && !isset($entity->original)) { + // In order to properly work in case of name changes, load the original + // entity using the id key if it is available. + $entity->original = entity_load_unchanged($this->entityType, $entity->id()); + } + + $this->preSave($entity); + $this->invokeHookPreSave($entity); + + if (!$entity->isNew()) { + $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); + $this->resetCache(array($entity->{$this->idKey})); + $this->postSave($entity); + $this->invokeHookUpdate($entity); + } + else { + $return = drupal_write_record($this->entityInfo['base table'], $entity); + $this->postSave($entity); + $this->invokeHookInsert($entity); + } + + // Ignore slave server temporarily. + db_ignore_slave(); + unset($entity->is_new); + unset($entity->original); + + return $return; + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception($this->entityType, $e); + throw $e; + } + } + + /** + * Act on an entity before the presave hook is invoked. + */ + protected function preSave(EntityInterface $entity) { } + + /** + * Act on an entity when inserted or updated before the respective hook is invoked. + */ + protected function postSave(EntityInterface $entity) { } + + /** + * Act on entities before they are deleted. + */ + protected function preDelete($entities) { } + + /** + * Act on entities when deleted before the delete hook is invoked. + */ + protected function postDelete($entities) { } + + /** + * Invokes hook_ENTITY_TYPE_presave() on behalf the entity. + */ + protected function invokeHookPreSave(EntityInterface $entity) { + if ($this->entityInfo['fieldable']) { + field_attach_presave($this->entityType, $entity); + } + module_invoke_all($this->entityType . '_presave', $entity); + module_invoke_all('entity_presave', $entity, $this->entityType); + } + + /** + * Invokes hook_ENTITY_TYPE_insert() on behalf the entity. + */ + protected function invokeHookInsert(EntityInterface $entity) { + if ($this->entityInfo['fieldable']) { + field_attach_insert($this->entityType, $entity); + } + module_invoke_all($this->entityType . '_insert', $entity); + module_invoke_all('entity_insert', $entity, $this->entityType); + } + + /** + * Invokes hook_ENTITY_TYPE_update() on behalf the entity. + */ + protected function invokeHookUpdate(EntityInterface $entity) { + if ($this->entityInfo['fieldable']) { + field_attach_update($this->entityType, $entity); + } + module_invoke_all($this->entityType . '_update', $entity); + module_invoke_all('entity_update', $entity, $this->entityType); + } + + /** + * Invokes hook_ENTITY_TYPE_delete() on behalf the entity. + */ + protected function invokeHookDelete(EntityInterface $entity) { + if ($this->entityInfo['fieldable']) { + field_attach_delete($this->entityType, $entity); + } + module_invoke_all($this->entityType . '_delete', $entity); + module_invoke_all('entity_delete', $entity, $this->entityType); + } +} + diff --git a/modules/entity/entity.info b/modules/entity/entity.info index b51e57d..b15fadd 100644 --- a/modules/entity/entity.info +++ b/modules/entity/entity.info @@ -4,6 +4,9 @@ package = Core version = VERSION core = 8.x required = TRUE +files[] = entity.class.inc files[] = entity.query.inc files[] = entity.controller.inc +files[] = tests/entity_crud_hook_test.test files[] = tests/entity_query.test +files[] = tests/entity.test diff --git a/modules/entity/entity.module b/modules/entity/entity.module index 02611e3..f8daee1 100644 --- a/modules/entity/entity.module +++ b/modules/entity/entity.module @@ -173,19 +173,20 @@ function entity_extract_ids($entity_type, $entity) { * 2: bundle name of the entity, or NULL if $entity_type has no bundles * * @return - * An entity structure, initialized with the ids provided. + * An entity object, initialized with the ids provided. */ function entity_create_stub_entity($entity_type, $ids) { - $entity = new stdClass(); + $values = array(); $info = entity_get_info($entity_type); - $entity->{$info['entity keys']['id']} = $ids[0]; + $values[$info['entity keys']['id']] = $ids[0]; if (!empty($info['entity keys']['revision']) && isset($ids[1])) { - $entity->{$info['entity keys']['revision']} = $ids[1]; + $values[$info['entity keys']['revision']] = $ids[1]; } if (!empty($info['entity keys']['bundle']) && isset($ids[2])) { - $entity->{$info['entity keys']['bundle']} = $ids[2]; + $values[$info['entity keys']['bundle']] = $ids[2]; } - return $entity; + // @todo: Once all entities are converted, just rely on entity_construct(). + return isset($info['entity class']) ? entity_construct($entity_type, $values) : (object) $values; } /** @@ -256,6 +257,36 @@ function entity_load_unchanged($entity_type, $id) { } /** + * Permanently delete multiple entities. + * + * @param $entity_type + * The type of the entity. + * @param $ids + * An array of entity ids of the entities to delete. + */ +function entity_delete_multiple($entity_type, $ids) { + entity_get_controller($entity_type)->delete($ids); +} + +/** + * Construct a new entity object. + * + * @param $entity_type + * The type of the entity. + * @param $values + * An array of values to set, keyed by property name. If the entity type has + * bundles the bundle key has to be specified. + * + * @return EntityInterface + * A new entity object. + */ +function entity_construct($entity_type, array $values) { + $info = entity_get_info($entity_type) + array('entity class' => 'Entity'); + $class = $info['entity class']; + return new $class($values, $entity_type); +} + +/** * Gets the entity controller class for an entity type. */ function entity_get_controller($entity_type) { @@ -321,6 +352,9 @@ function entity_prepare_view($entity_type, $entities) { * An array containing the 'path' and 'options' keys used to build the uri of * the entity, and matching the signature of url(). NULL if the entity has no * uri of its own. + * + * @todo + * Remove once all entity types are implementing the EntityInterface. */ function entity_uri($entity_type, $entity) { $info = entity_get_info($entity_type); @@ -362,6 +396,9 @@ function entity_uri($entity_type, $entity) { * * @return * The entity label, or FALSE if not found. + * + * @todo + * Remove once all entity types are implementing the EntityInterface. */ function entity_label($entity_type, $entity) { $label = FALSE; diff --git a/modules/entity/tests/entity.test b/modules/entity/tests/entity.test new file mode 100644 index 0000000..9dd0247 --- /dev/null +++ b/modules/entity/tests/entity.test @@ -0,0 +1,69 @@ + 'Entity CRUD', + 'description' => 'Tests basic CRUD functionality.', + 'group' => 'Entity API', + ); + } + + function setUp() { + parent::setUp('entity', 'entity_test'); + } + + /** + * Tests basic CRUD functionality of the entity API. + */ + function testCRUD() { + $user1 = $this->drupalCreateUser(); + + // Create some test entities. + $entity = entity_construct('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + $entity->save(); + $entity = entity_construct('entity_test', array('name' => 'test2', 'uid' => $user1->uid)); + $entity->save(); + $entity = entity_construct('entity_test', array('name' => 'test', 'uid' => NULL)); + $entity->save(); + + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test'))); + + $this->assertEqual($entities[0]->name, 'test', 'Created and loaded entity.'); + $this->assertEqual($entities[1]->name, 'test', 'Created and loaded entity.'); + + // Test loading a single entity. + $loaded_entity = entity_test_load($entity->id); + $this->assertEqual($loaded_entity->id, $entity->id, 'Loaded a single entity by id.'); + + // Test deleting an entity. + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + $entities[0]->delete(); + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + $this->assertEqual($entities, array(), 'Entity deleted.'); + + // Test updating an entity. + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test'))); + $entities[0]->name = 'test3'; + $entities[0]->save(); + $entity = entity_test_load($entities[0]->id); + $this->assertEqual($entity->name, 'test3', 'Entity updated.'); + + // Try deleting multiple test entities by deleting all. + $ids = array_keys(entity_test_load_multiple(FALSE)); + entity_test_delete_multiple($ids); + + $all = entity_test_load_multiple(FALSE); + $this->assertTrue(empty($all), 'Deleted all entities.'); + } +} + diff --git a/modules/entity/tests/entity_crud_hook_test.test b/modules/entity/tests/entity_crud_hook_test.test index 3f18fc8..1663206 100644 --- a/modules/entity/tests/entity_crud_hook_test.test +++ b/modules/entity/tests/entity_crud_hook_test.test @@ -66,7 +66,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase { node_save($node); $nid = $node->nid; - $comment = (object) array( + $comment = entity_construct('comment', array( 'cid' => NULL, 'pid' => 0, 'nid' => $nid, @@ -76,7 +76,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase { 'changed' => REQUEST_TIME, 'status' => 1, 'language' => LANGUAGE_NONE, - ); + )); $_SESSION['entity_crud_hook_test'] = array(); comment_save($comment); diff --git a/modules/entity/tests/entity_test.info b/modules/entity/tests/entity_test.info new file mode 100644 index 0000000..ae6006b --- /dev/null +++ b/modules/entity/tests/entity_test.info @@ -0,0 +1,7 @@ +name = Entity CRUD test module +description = Provides entity types based upon the CRUD API. +package = Testing +version = VERSION +core = 8.x +dependencies[] = entity +hidden = TRUE diff --git a/modules/entity/tests/entity_test.install b/modules/entity/tests/entity_test.install new file mode 100644 index 0000000..ec2e5bd --- /dev/null +++ b/modules/entity/tests/entity_test.install @@ -0,0 +1,70 @@ + 'field_test_text', + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => FALSE, + ); + field_create_field($field); + + $instance = array( + 'entity_type' => 'entity_test', + 'field_name' => 'field_test_text', + 'bundle' => 'entity_test', + 'label' => 'Test text-field', + 'widget' => array( + 'type' => 'text_textfield', + 'weight' => 0, + ), + ); + field_create_instance($instance); +} + +/** + * Implements hook_schema(). + */ +function entity_test_schema() { + $schema['entity_test'] = array( + 'description' => 'Stores entity_test items.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique entity-test item ID.', + ), + 'name' => array( + 'description' => 'The name of the test entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the associated user.", + ), + ), + 'indexes' => array( + 'uid' => array('uid'), + ), + 'foreign keys' => array( + 'uid' => array('users' => 'uid'), + ), + 'primary key' => array('id'), + ); + return $schema; +} diff --git a/modules/entity/tests/entity_test.module b/modules/entity/tests/entity_test.module new file mode 100644 index 0000000..83da7d6 --- /dev/null +++ b/modules/entity/tests/entity_test.module @@ -0,0 +1,68 @@ + array( + 'label' => t('Test entity'), + 'entity class' => 'Entity', + 'controller class' => 'EntityDatabaseStorageController', + 'base table' => 'entity_test', + 'fieldable' => TRUE, + 'entity keys' => array( + 'id' => 'id', + ), + ), + ); + return $return; +} + +/** + * Load a test-entity. + * + * @param $id + * A test-entity id. + * @param $reset + * A boolean indicating that the internal cache should be reset. + * + * @return Entity + * The loaded entity object or FALSE. + */ +function entity_test_load($id, $reset = FALSE) { + $result = entity_load('entity_test', array($id), array(), $reset); + return reset($result); +} + +/** + * Loads multiple test entities based on certain conditions. + * + * @param $ids + * An array of entity IDs. + * @param $conditions + * An array of conditions to match against the {entity} table. + * @param $reset + * A boolean indicating that the internal cache should be reset. + * + * @return + * An array of test entity objects, indexed by id. + */ +function entity_test_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) { + return entity_load('entity_test', $ids, $conditions, $reset); +} + +/** + * Deletes multiple test entities. + * + * @param $ids + * An array of test entity IDs. + */ +function entity_test_delete_multiple(array $ids) { + entity_get_controller('entity_test')->delete($ids); +} diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index ba817da..c86f440 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -15,8 +15,6 @@ files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test -files[] = tests/entity_crud_hook_test.test -files[] = tests/entity_query.test files[] = tests/error.test files[] = tests/file.test files[] = tests/filetransfer.test diff --git a/modules/user/user.test b/modules/user/user.test index 6ecbfac..edd127f 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -732,7 +732,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview')); $this->drupalPost(NULL, array(), t('Save')); $this->assertText(t('Your comment has been posted.')); - $comments = comment_load_multiple(array(), array('subject' => $edit['subject'])); + $comments = comment_load_multiple(FALSE, array('subject' => $edit['subject'])); $comment = reset($comments); $this->assertTrue($comment->cid, t('Comment found.'));