Issue #1184944 by fago, xjm, klausi, aspilicious, bojanz, Tor Arne Thune: Make entities classed objects, introduce CRUD support.

8.0.x
catch 2011-12-05 21:12:15 +09:00
parent faddd57042
commit 694b6ac9c8
15 changed files with 1042 additions and 308 deletions

View File

@ -0,0 +1,280 @@
<?php
/**
* @file
* Entity controller and class for comments.
*/
/**
* Defines the comment entity class.
*/
class Comment extends Entity {
/**
* The comment ID.
*
* @var integer
*/
public $cid;
/**
* The parent comment ID if this is a reply to a comment.
*
* @var integer
*/
public $pid;
/**
* The comment language.
*
* @var string
*/
public $language = LANGUAGE_NONE;
/**
* The comment title.
*
* @var string
*/
public $subject;
/**
* The comment author ID.
*
* @var integer
*/
public $uid = 0;
/**
* The comment author's name.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @var string
*/
public $name = '';
/**
* The comment author's e-mail address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @var string
*/
public $mail;
/**
* The comment author's home page address.
*
* For anonymous authors, this is the value as typed in the comment form.
*
* @var string
*/
public $homepage;
}
/**
* Defines the controller class for comments.
*
* This extends the EntityDatabaseStorageController class, adding required
* special handling for comment entities.
*/
class CommentStorageController extends EntityDatabaseStorageController {
/**
* Overrides EntityDatabaseStorageController::buildQuery().
*/
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;
}
/**
* Overrides EntityDatabaseStorageController::attachLoad().
*/
protected function attachLoad(&$comments, $revision_id = FALSE) {
// Set up 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);
}
/**
* Overrides EntityDatabaseStorageController::preSave().
*
* @see comment_int_to_alphadecimal()
* @see comment_increment_alphadecimal()
*/
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_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;
}
// We test the value with '===' because we need to modify anonymous
// users as well.
if ($comment->uid === $user->uid && isset($user->name)) {
$comment->name = $user->name;
}
// Add the values which aren't passed into the function.
$comment->thread = $thread;
$comment->hostname = ip_address();
}
}
/**
* Overrides EntityDatabaseStorageController::postSave().
*/
protected function postSave(EntityInterface $comment) {
// Update the {node_comment_statistics} table prior to executing the hook.
$this->updateNodeStatistics($comment->nid);
if ($comment->status == COMMENT_PUBLISHED) {
module_invoke_all('comment_publish', $comment);
}
}
/**
* Overrides EntityDatabaseStorageController::postDelete().
*/
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 {node_comment_statistics} table has the following fields:
* - last_comment_timestamp: The timestamp of the last comment for this node,
* or the node created timestamp if no comments exist for the node.
* - last_comment_name: The name of the anonymous poster for the last comment.
* - last_comment_uid: The user ID of the poster for the last comment for
* this node, or the node author's user ID if no comments exist for the
* node.
* - comment_count: The total number of approved/published comments on this
* node.
*
* @param $nid
* The node ID.
*/
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();
}
}
}

View File

@ -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

View File

@ -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',
@ -738,8 +739,8 @@ 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));
$additions['comment_form'] = $build;
$comment = entity_create('comment', array('nid' => $node->nid));
$additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
}
if ($additions) {
@ -1436,151 +1437,9 @@ function comment_access($op, $comment) {
*
* @param $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;
}
$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);
}
/**
@ -1671,37 +1506,6 @@ function comment_load($cid, $reset = FALSE) {
return $comment ? $comment[$cid] : FALSE;
}
/**
* Controller class for comments.
*
* This extends the DrupalDefaultEntityController class, adding required
* special handling for comment objects.
*/
class CommentController extends DrupalDefaultEntityController {
protected function buildQuery($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.
*
@ -1831,22 +1635,6 @@ function comment_form($form, &$form_state, $comment) {
// 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 {
@ -2143,12 +1931,6 @@ 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;
}
if (empty($comment->date)) {
$comment->date = 'now';
}
@ -2398,61 +2180,6 @@ function _comment_per_page() {
return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
}
/**
* 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.
*

View File

@ -35,7 +35,8 @@ 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));
$comment = entity_create('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');
@ -86,8 +87,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_create('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');

View File

@ -87,7 +87,7 @@ class CommentHelperCase extends DrupalWebTestCase {
}
if (isset($match[1])) {
return (object) array('id' => $match[1], 'subject' => $subject, 'comment' => $comment);
return entity_create('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_create('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_create('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_create('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_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body));
$this->drupalLogout();
$this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.'));

View File

@ -0,0 +1,300 @@
<?php
/**
* @file
* Provides an interface and a base class for entities.
*/
/**
* Defines a common interface for all entity objects.
*/
interface EntityInterface {
/**
* Constructs a new entity object.
*
* @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.
* @param $entity_type
* The type of the entity to create.
*/
public function __construct(array $values, $entity_type);
/**
* Returns the entity identifier (the entity's machine name or numeric ID).
*
* @return
* The identifier of the entity, or NULL if the entity does not yet have
* an identifier.
*/
public function id();
/**
* Returns whether the entity is new.
*
* @return
* TRUE if the entity is new, or FALSE if the entity has already been saved.
*/
public function isNew();
/**
* Returns the type of the entity.
*
* @return
* The type of the entity.
*/
public function entityType();
/**
* Returns the bundle of the entity.
*
* @return
* The bundle of the entity. Defaults to the entity type if the entity type
* does not make use of different bundles.
*/
public function bundle();
/**
* Returns the label of the entity.
*
* @return
* The label of the entity, or NULL if there is no label defined.
*/
public function label();
/**
* Returns the URI elements of the entity.
*
* @return
* 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.
*/
public function uri();
/**
* Saves an entity permanently.
*
* @return
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
*
* @throws EntityStorageException
* In case of failures an exception is thrown.
*/
public function save();
/**
* Deletes an entity permanently.
*
* @throws EntityStorageException
* In case of failures an exception is thrown.
*/
public function delete();
/**
* Creates a duplicate of the entity.
*
* @return EntityInterface
* A clone of the current entity with all identifiers unset, so saving
* it inserts a new entity into the storage system.
*/
public function createDuplicate();
/**
* Returns the info of the type of the entity.
*
* @see entity_get_info()
*/
public function entityInfo();
}
/**
* Defines a base entity class.
*
* Default implementation of EntityInterface.
*
* This class can be used as-is by simple entity types. Entity types requiring
* special handling can extend the class.
*/
class Entity implements EntityInterface {
/**
* The entity type.
*
* @var string
*/
protected $entityType;
/**
* Information about the entity's type.
*
* @var array
*/
protected $entityInfo;
/**
* The entity ID key.
*
* @var string
*/
protected $idKey;
/**
* The entity bundle key.
*
* @var string
*/
protected $bundleKey;
/**
* Constructs a new entity object.
*/
public function __construct(array $values = array(), $entity_type) {
$this->entityType = $entity_type;
$this->setUp();
// Set initial values.
foreach ($values as $key => $value) {
$this->$key = $value;
}
}
/**
* Sets up the object instance on construction or unserialization.
*/
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;
}
/**
* Implements EntityInterface::id().
*/
public function id() {
return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL;
}
/**
* Implements EntityInterface::isNew().
*/
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});
}
/**
* Implements EntityInterface::entityType().
*/
public function entityType() {
return $this->entityType;
}
/**
* Implements EntityInterface::bundle().
*/
public function bundle() {
return isset($this->bundleKey) ? $this->{$this->bundleKey} : $this->entityType;
}
/**
* Implements EntityInterface::label().
*
* @see entity_label()
*/
public function 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($this->{$this->entityInfo['entity keys']['label']})) {
$label = $this->{$this->entityInfo['entity keys']['label']};
}
return $label;
}
/**
* Implements EntityInterface::uri().
*
* @see entity_uri()
*/
public function 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
// look up this entity again.
$uri['options']['entity_type'] = $this->entityType;
$uri['options']['entity'] = $this;
return $uri;
}
}
/**
* Implements EntityInterface::save().
*/
public function save() {
return entity_get_controller($this->entityType)->save($this);
}
/**
* Implements EntityInterface::delete().
*/
public function delete() {
if (!$this->isNew()) {
entity_get_controller($this->entityType)->delete(array($this->id()));
}
}
/**
* Implements EntityInterface::createDuplicate().
*/
public function createDuplicate() {
$duplicate = clone $this;
$duplicate->{$this->idKey} = NULL;
return $duplicate;
}
/**
* Implements EntityInterface::entityInfo().
*/
public function entityInfo() {
return $this->entityInfo;
}
/**
* Serializes only what is necessary.
*
* See @link http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.sleep PHP Magic Methods @endlink.
*/
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));
}
/**
* Invokes setUp() on unserialization.
*
* See @link http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.sleep PHP Magic Methods @endlink
*/
public function __wakeup() {
$this->setUp();
}
}

View File

@ -6,7 +6,7 @@
*/
/**
* Interface for entity controller classes.
* Defines a common interface for entity controller classes.
*
* All entity controller classes specified via the 'controller class' key
* returned by hook_entity_info() or hook_entity_info_alter() have to implement
@ -19,7 +19,7 @@
interface DrupalEntityControllerInterface {
/**
* Constructor.
* Constructs a new DrupalEntityControllerInterface object.
*
* @param $entityType
* The entity type for which the instance is created.
@ -50,6 +50,8 @@ interface DrupalEntityControllerInterface {
}
/**
* Defines a base entity controller class.
*
* Default implementation of DrupalEntityControllerInterface.
*
* This class can be used as-is by most simple entity types. Entity types
@ -122,7 +124,9 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
protected $cache;
/**
* Constructor: sets basic variables.
* Implements DrupalEntityControllerInterface::__construct().
*
* Sets basic variables.
*/
public function __construct($entityType) {
$this->entityType = $entityType;
@ -194,11 +198,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'])) {
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @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 +397,169 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
$this->entityCache += $entities;
}
}
/**
* Defines a common interface for entity storage controllers.
*/
interface EntityStorageControllerInterface extends DrupalEntityControllerInterface {
/**
* Deletes permanently saved entities.
*
* @param $ids
* An array of entity IDs.
*
* @throws EntityStorageException
* In case of failures, an exception is thrown.
*/
public function delete($ids);
/**
* Saves the entity permanently.
*
* @param EntityInterface $entity
* The entity to save.
*
* @return
* SAVED_NEW or SAVED_UPDATED is returned depending on the operation
* performed.
*
* @throws EntityStorageException
* In case of failures, an exception is thrown.
*/
public function save(EntityInterface $entity);
}
/**
* Defines an exception thrown when storage operations fail.
*/
class EntityStorageException extends Exception { }
/**
* Implements the entity storage controller interface for the database.
*/
class EntityDatabaseStorageController extends DrupalDefaultEntityController implements EntityStorageControllerInterface {
/**
* Implements EntityStorageControllerInterface::delete().
*/
public function delete($ids) {
$entities = $ids ? $this->load($ids) : FALSE;
if (!$entities) {
// If no IDs or invalid IDs were passed, do nothing.
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->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
}
catch (Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e->getMessage, $e->getCode, $e);
}
}
/**
* Implements EntityStorageControllerInterface::save().
*/
public function save(EntityInterface $entity) {
$transaction = db_transaction();
try {
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
}
$this->preSave($entity);
$this->invokeHook('presave', $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->invokeHook('update', $entity);
}
else {
$return = drupal_write_record($this->entityInfo['base table'], $entity);
$this->postSave($entity);
$this->invokeHook('insert', $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 new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*/
protected function preSave(EntityInterface $entity) { }
/**
* Acts on a saved entity before the insert or update hook is invoked.
*
* Used after the entity is saved, but before invoking the insert or update
* hook.
*/
protected function postSave(EntityInterface $entity) { }
/**
* Acts on entities before they are deleted.
*
* Used before the entities are deleted and before invoking the delete hook.
*/
protected function preDelete($entities) { }
/**
* Acts on deleted entities before the delete hook is invoked.
*
* Used after the entities are deleted but before invoking the delete hook.
*/
protected function postDelete($entities) { }
/**
* Invokes a hook on behalf of the entity.
*
* @param $op
* One of 'presave', 'insert', 'update', or 'delete'.
* @param $entity
* The entity object.
*/
protected function invokeHook($hook, EntityInterface $entity) {
if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
$function($this->entityType, $entity);
}
// Invoke the hook.
module_invoke_all($this->entityType . '_' . $hook, $entity);
// Invoke the respective entity-level hook.
module_invoke_all('entity_' . $hook, $entity, $this->entityType);
}
}

View File

@ -4,7 +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

View File

@ -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_create().
return isset($info['entity class']) ? entity_create($entity_type, $values) : (object) $values;
}
/**
@ -255,6 +256,36 @@ function entity_load_unchanged($entity_type, $id) {
return reset($result);
}
/**
* Deletes multiple entities permanently.
*
* @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);
}
/**
* Constructs a new entity object, without saving it to the database.
*
* @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_create($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.
*/
@ -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;
@ -439,4 +476,3 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
* Exception thrown when a malformed entity is passed.
*/
class EntityMalformedException extends Exception { }

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Entity CRUD API tests.
*/
/**
* Tests the basic Entity API.
*/
class EntityAPITestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => '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_create('entity_test', array('name' => 'test', 'uid' => $user1->uid));
$entity->save();
$entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid));
$entity->save();
$entity = entity_create('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.');
}
}

View File

@ -66,7 +66,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
node_save($node);
$nid = $node->nid;
$comment = (object) array(
$comment = entity_create('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);

View File

@ -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

View File

@ -0,0 +1,70 @@
<?php
/**
* @file
* Install, update and uninstall functions for the entity_test module.
*/
/**
* Implements hook_install().
*/
function entity_test_install() {
// Auto-create a field for testing.
$field = array(
'field_name' => '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;
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Test module for the entity API providing an entity type for testing.
*/
/**
* Implements hook_entity_info().
*/
function entity_test_entity_info() {
$return = array(
'entity_test' => array(
'label' => t('Test entity'),
'entity class' => 'Entity',
'controller class' => 'EntityDatabaseStorageController',
'base table' => 'entity_test',
'fieldable' => TRUE,
'entity keys' => array(
'id' => 'id',
),
),
);
return $return;
}
/**
* Loads 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 if the entity cannot be loaded.
*/
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);
}

View File

@ -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.'));